Add OIDC login support to compliance page
Add Google and Microsoft sign-in buttons to the trust center connect page, matching the console sign-in experience. The backend OIDC flow already supports flexible continue URLs, so only the GraphQL schema and frontend needed changes. Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
import type { GraphQLError } from "@probo/helpers";
|
import type { GraphQLError } from "@probo/helpers";
|
||||||
import { usePageTitle } from "@probo/hooks";
|
import { usePageTitle } from "@probo/hooks";
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import { Button, Field, useToast } from "@probo/ui";
|
import { Button, Field, Google, Microsoft, useToast } from "@probo/ui";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { type ComponentProps, Suspense, useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
type PreloadedQuery,
|
type PreloadedQuery,
|
||||||
|
useLazyLoadQuery,
|
||||||
useMutation,
|
useMutation,
|
||||||
usePreloadedQuery,
|
usePreloadedQuery,
|
||||||
} from "react-relay";
|
} from "react-relay";
|
||||||
@@ -16,6 +17,7 @@ import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
|||||||
import { getPathPrefix } from "#/utils/pathPrefix";
|
import { getPathPrefix } from "#/utils/pathPrefix";
|
||||||
|
|
||||||
import type { ConnectPageMutation, SendMagicLinkInput } from "./__generated__/ConnectPageMutation.graphql";
|
import type { ConnectPageMutation, SendMagicLinkInput } from "./__generated__/ConnectPageMutation.graphql";
|
||||||
|
import type { ConnectPageOIDCQuery } from "./__generated__/ConnectPageOIDCQuery.graphql";
|
||||||
import type { ConnectPageQuery } from "./__generated__/ConnectPageQuery.graphql";
|
import type { ConnectPageQuery } from "./__generated__/ConnectPageQuery.graphql";
|
||||||
|
|
||||||
export const connectPageQuery = graphql`
|
export const connectPageQuery = graphql`
|
||||||
@@ -28,6 +30,15 @@ export const connectPageQuery = graphql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const oidcProvidersQuery = graphql`
|
||||||
|
query ConnectPageOIDCQuery {
|
||||||
|
oidcProviders {
|
||||||
|
name
|
||||||
|
loginURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const sendMagicLinkMutation = graphql`
|
const sendMagicLinkMutation = graphql`
|
||||||
mutation ConnectPageMutation($input: SendMagicLinkInput!) {
|
mutation ConnectPageMutation($input: SendMagicLinkInput!) {
|
||||||
sendMagicLink(input: $input) {
|
sendMagicLink(input: $input) {
|
||||||
@@ -36,6 +47,14 @@ const sendMagicLinkMutation = graphql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const providerIcons: Record<
|
||||||
|
string,
|
||||||
|
(props: ComponentProps<"svg">) => React.ReactNode
|
||||||
|
> = {
|
||||||
|
google: Google,
|
||||||
|
microsoft: Microsoft,
|
||||||
|
};
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
});
|
});
|
||||||
@@ -44,6 +63,58 @@ type FormData = z.infer<typeof schema>;
|
|||||||
|
|
||||||
const timerDurationSeconds = 60;
|
const timerDurationSeconds = 60;
|
||||||
|
|
||||||
|
function Divider({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="relative my-6 w-full">
|
||||||
|
<div className="border-t border-border-mid" />
|
||||||
|
<span className="px-4 text-xs uppercase text-txt-secondary bg-level-0 absolute top-0 left-1/2 -translate-1/2">
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function OIDCButtons({ safeContinueUrl }: { safeContinueUrl: string }) {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
|
||||||
|
const data = useLazyLoadQuery<ConnectPageOIDCQuery>(oidcProvidersQuery, {});
|
||||||
|
|
||||||
|
if (data.oidcProviders.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const continueUrl = new URL(safeContinueUrl);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{data.oidcProviders.map((provider) => {
|
||||||
|
const Icon = providerIcons[provider.name];
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={provider.name}
|
||||||
|
variant="secondary"
|
||||||
|
className="w-full h-10"
|
||||||
|
onClick={() => {
|
||||||
|
window.location.href
|
||||||
|
= provider.loginURL
|
||||||
|
+ "?continue="
|
||||||
|
+ encodeURIComponent(
|
||||||
|
continueUrl.pathname + continueUrl.search,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
{Icon && <Icon width={18} height={18} />}
|
||||||
|
{__(`Sign in with ${provider.name.charAt(0).toUpperCase() + provider.name.slice(1)}`)}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Divider>{__("Or")}</Divider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ConnectPage(props: {
|
export function ConnectPage(props: {
|
||||||
queryRef: PreloadedQuery<ConnectPageQuery>;
|
queryRef: PreloadedQuery<ConnectPageQuery>;
|
||||||
}) {
|
}) {
|
||||||
@@ -164,11 +235,17 @@ export function ConnectPage(props: {
|
|||||||
</h1>
|
</h1>
|
||||||
<p className="text-txt-tertiary">
|
<p className="text-txt-tertiary">
|
||||||
{__(
|
{__(
|
||||||
"Enter your email address to connect with a magic link and start requesting access to documents",
|
"Sign in to start requesting access to documents",
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<OIDCButtons safeContinueUrl={safeContinueUrl} />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form onSubmit={e => void handleSubmit(e)} className="space-y-6">
|
<form onSubmit={e => void handleSubmit(e)} className="space-y-6">
|
||||||
<Field
|
<Field
|
||||||
label={__("Email")}
|
label={__("Email")}
|
||||||
|
|||||||
@@ -880,10 +880,18 @@ type RecordSigningEventPayload {
|
|||||||
success: Boolean!
|
success: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OIDCProviderInfo {
|
||||||
|
name: String!
|
||||||
|
loginURL: String!
|
||||||
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
viewer: Identity
|
viewer: Identity
|
||||||
node(id: ID!): Node
|
node(id: ID!): Node
|
||||||
currentTrustCenter: TrustCenter
|
currentTrustCenter: TrustCenter
|
||||||
|
oidcProviders: [OIDCProviderInfo!]!
|
||||||
|
@goField(forceResolver: true)
|
||||||
|
@session(required: OPTIONAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.gearno.de/kit/log"
|
"go.gearno.de/kit/log"
|
||||||
@@ -993,6 +994,22 @@ func (r *queryResolver) CurrentTrustCenter(ctx context.Context) (*types.TrustCen
|
|||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OidcProviders is the resolver for the oidcProviders field.
|
||||||
|
func (r *queryResolver) OidcProviders(ctx context.Context) ([]*types.OIDCProviderInfo, error) {
|
||||||
|
providers := r.iam.OIDCService.EnabledProviders()
|
||||||
|
result := make([]*types.OIDCProviderInfo, 0, len(providers))
|
||||||
|
|
||||||
|
for _, p := range providers {
|
||||||
|
name := strings.ToLower(p.String())
|
||||||
|
result = append(result, &types.OIDCProviderInfo{
|
||||||
|
Name: name,
|
||||||
|
LoginURL: r.baseURL.WithPath("/api/connect/v1/oidc/" + name + "/login").MustString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
// IsUserAuthorized is the resolver for the isUserAuthorized field.
|
// IsUserAuthorized is the resolver for the isUserAuthorized field.
|
||||||
func (r *reportResolver) IsUserAuthorized(ctx context.Context, obj *types.Report) (bool, error) {
|
func (r *reportResolver) IsUserAuthorized(ctx context.Context, obj *types.Report) (bool, error) {
|
||||||
trustService := r.TrustService(ctx, obj.ID.TenantID())
|
trustService := r.TrustService(ctx, obj.ID.TenantID())
|
||||||
|
|||||||
Reference in New Issue
Block a user