import { zodResolver } from '@hookform/resolvers/zod'; import { msg } from '@lingui/core/macro'; import { Trans, useLingui } from '@lingui/react/macro'; import { OrganisationMemberRole } from '@prisma/client'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { ORGANISATION_MEMBER_ROLE_HIERARCHY } from '@documenso/lib/constants/organisations'; import { ORGANISATION_MEMBER_ROLE_MAP } from '@documenso/lib/constants/organisations-translations'; import { formatOrganisationCallbackUrl, formatOrganisationLoginUrl, } from '@documenso/lib/utils/organisation-authentication-portal'; import { trpc } from '@documenso/trpc/react'; import { domainRegex } from '@documenso/trpc/server/enterprise-router/create-organisation-email-domain.types'; import type { TGetOrganisationAuthenticationPortalResponse } from '@documenso/trpc/server/enterprise-router/get-organisation-authentication-portal.types'; import { ZUpdateOrganisationAuthenticationPortalRequestSchema } from '@documenso/trpc/server/enterprise-router/update-organisation-authentication-portal.types'; import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Button } from '@documenso/ui/primitives/button'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; import { Label } from '@documenso/ui/primitives/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@documenso/ui/primitives/select'; import { SpinnerBox } from '@documenso/ui/primitives/spinner'; import { Switch } from '@documenso/ui/primitives/switch'; import { Textarea } from '@documenso/ui/primitives/textarea'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { SettingsHeader } from '~/components/general/settings-header'; import { appMetaTags } from '~/utils/meta'; const ZProviderFormSchema = ZUpdateOrganisationAuthenticationPortalRequestSchema.shape.data .pick({ enabled: true, wellKnownUrl: true, clientId: true, autoProvisionUsers: true, defaultOrganisationRole: true, }) .extend({ clientSecret: z.string().nullable(), allowedDomains: z.string().refine( (value) => { const domains = value.split(' ').filter(Boolean); return domains.every((domain) => domainRegex.test(domain)); }, { message: msg`Invalid domains`.id, }, ), }); type TProviderFormSchema = z.infer; export function meta() { return appMetaTags('Organisation SSO Portal'); } export default function OrganisationSettingSSOLoginPage() { const { t } = useLingui(); const organisation = useCurrentOrganisation(); const { data: authenticationPortal, isLoading: isLoadingAuthenticationPortal } = trpc.enterprise.organisation.authenticationPortal.get.useQuery({ organisationId: organisation.id, }); if (isLoadingAuthenticationPortal || !authenticationPortal) { return ; } return (
); } type SSOProviderFormProps = { authenticationPortal: TGetOrganisationAuthenticationPortalResponse; }; const SSOProviderForm = ({ authenticationPortal }: SSOProviderFormProps) => { const { t } = useLingui(); const { toast } = useToast(); const organisation = useCurrentOrganisation(); const { mutateAsync: updateOrganisationAuthenticationPortal } = trpc.enterprise.organisation.authenticationPortal.update.useMutation(); const form = useForm({ resolver: zodResolver(ZProviderFormSchema), defaultValues: { enabled: authenticationPortal.enabled, clientId: authenticationPortal.clientId, clientSecret: authenticationPortal.clientSecretProvided ? null : '', wellKnownUrl: authenticationPortal.wellKnownUrl, autoProvisionUsers: authenticationPortal.autoProvisionUsers, defaultOrganisationRole: authenticationPortal.defaultOrganisationRole, allowedDomains: authenticationPortal.allowedDomains.join(' '), }, }); const onSubmit = async (values: TProviderFormSchema) => { const { enabled, clientId, clientSecret, wellKnownUrl } = values; if (enabled && !clientId) { form.setError('clientId', { message: t`Client ID is required`, }); return; } if (enabled && clientSecret === '') { form.setError('clientSecret', { message: t`Client secret is required`, }); return; } if (enabled && !wellKnownUrl) { form.setError('wellKnownUrl', { message: t`Well-known URL is required`, }); return; } try { await updateOrganisationAuthenticationPortal({ organisationId: organisation.id, data: { enabled, clientId, clientSecret: values.clientSecret ?? undefined, wellKnownUrl, autoProvisionUsers: values.autoProvisionUsers, defaultOrganisationRole: values.defaultOrganisationRole, allowedDomains: values.allowedDomains.split(' ').filter(Boolean), }, }); toast({ title: t`Success`, description: t`Provider has been updated successfully`, duration: 5000, }); } catch (err) { console.error(err); toast({ title: t`An error occurred`, description: t`We couldn't update the provider. Please try again.`, variant: 'destructive', }); } }; const isSsoEnabled = form.watch('enabled'); return (
toast({ title: t`Copied to clipboard` })} />

This is the URL which users will use to sign in to your organisation.

toast({ title: t`Copied to clipboard` })} />

Add this URL to your provider's allowed redirect URIs

This is the required scopes you must set in your provider's settings

( Issuer URL {!form.formState.errors.wellKnownUrl && (

The OpenID discovery endpoint URL for your provider

)}
)} />
( Client ID )} /> ( Client Secret )} />
( Default Organisation Role for New Users )} /> ( Allowed Email Domains