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 { usePageTitle } from "@probo/hooks";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Button, Field, useToast } from "@probo/ui";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Button, Field, Google, Microsoft, useToast } from "@probo/ui";
|
||||
import { type ComponentProps, Suspense, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
type PreloadedQuery,
|
||||
useLazyLoadQuery,
|
||||
useMutation,
|
||||
usePreloadedQuery,
|
||||
} from "react-relay";
|
||||
@@ -16,6 +17,7 @@ import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||
import { getPathPrefix } from "#/utils/pathPrefix";
|
||||
|
||||
import type { ConnectPageMutation, SendMagicLinkInput } from "./__generated__/ConnectPageMutation.graphql";
|
||||
import type { ConnectPageOIDCQuery } from "./__generated__/ConnectPageOIDCQuery.graphql";
|
||||
import type { ConnectPageQuery } from "./__generated__/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`
|
||||
mutation ConnectPageMutation($input: SendMagicLinkInput!) {
|
||||
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({
|
||||
email: z.string().email(),
|
||||
});
|
||||
@@ -44,6 +63,58 @@ type FormData = z.infer<typeof schema>;
|
||||
|
||||
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: {
|
||||
queryRef: PreloadedQuery<ConnectPageQuery>;
|
||||
}) {
|
||||
@@ -164,11 +235,17 @@ export function ConnectPage(props: {
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Suspense fallback={null}>
|
||||
<OIDCButtons safeContinueUrl={safeContinueUrl} />
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
<form onSubmit={e => void handleSubmit(e)} className="space-y-6">
|
||||
<Field
|
||||
label={__("Email")}
|
||||
|
||||
@@ -880,10 +880,18 @@ type RecordSigningEventPayload {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type OIDCProviderInfo {
|
||||
name: String!
|
||||
loginURL: String!
|
||||
}
|
||||
|
||||
type Query {
|
||||
viewer: Identity
|
||||
node(id: ID!): Node
|
||||
currentTrustCenter: TrustCenter
|
||||
oidcProviders: [OIDCProviderInfo!]!
|
||||
@goField(forceResolver: true)
|
||||
@session(required: OPTIONAL)
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.gearno.de/kit/log"
|
||||
@@ -993,6 +994,22 @@ func (r *queryResolver) CurrentTrustCenter(ctx context.Context) (*types.TrustCen
|
||||
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.
|
||||
func (r *reportResolver) IsUserAuthorized(ctx context.Context, obj *types.Report) (bool, error) {
|
||||
trustService := r.TrustService(ctx, obj.ID.TenantID())
|
||||
|
||||
Reference in New Issue
Block a user