import { useState } from 'react'; import { msg } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; import { AlertTriangle, Building2, Database, Eye, Settings, UserCircle2 } from 'lucide-react'; import { data, isRouteErrorResponse } from 'react-router'; import { useNavigate } from 'react-router'; import { match } from 'ts-pattern'; import { ORGANISATION_ACCOUNT_LINK_VERIFICATION_TOKEN_IDENTIFIER } from '@documenso/lib/constants/organisations'; import { ZOrganisationAccountLinkMetadataSchema } from '@documenso/lib/types/organisation'; import { formatAvatarUrl } from '@documenso/lib/utils/avatars'; import { formatOrganisationLoginPath } from '@documenso/lib/utils/organisation-authentication-portal'; import { extractInitials } from '@documenso/lib/utils/recipient-formatter'; import { prisma } from '@documenso/prisma'; import { trpc } from '@documenso/trpc/react'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { AvatarWithText } from '@documenso/ui/primitives/avatar'; import { Badge } from '@documenso/ui/primitives/badge'; import { Button } from '@documenso/ui/primitives/button'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from '@documenso/ui/primitives/card'; import { Checkbox } from '@documenso/ui/primitives/checkbox'; import { Separator } from '@documenso/ui/primitives/separator'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { GenericErrorLayout, defaultErrorCodeMap } from '~/components/general/generic-error-layout'; import type { Route } from './+types/organisation.sso.confirmation.$token'; export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { const errorCode = isRouteErrorResponse(error) ? error.data.type : 500; const errorMap = match(errorCode) .with('invalid-token', () => ({ subHeading: msg`400 Error`, heading: msg`Invalid Token`, message: msg`The token is invalid or has expired.`, })) .otherwise(() => defaultErrorCodeMap[500]); return ( ); } export async function loader({ params }: Route.LoaderArgs) { const { token } = params; if (!token) { throw data({ type: 'invalid-token', }); } const verificationToken = await prisma.verificationToken.findFirst({ where: { token, identifier: ORGANISATION_ACCOUNT_LINK_VERIFICATION_TOKEN_IDENTIFIER, }, include: { user: { select: { name: true, email: true, avatarImageId: true, }, }, }, }); if (!verificationToken || verificationToken.expires < new Date()) { throw data({ type: 'invalid-token', }); } const metadata = ZOrganisationAccountLinkMetadataSchema.safeParse(verificationToken.metadata); if (!metadata.success) { throw data({ type: 'invalid-token', }); } const organisation = await prisma.organisation.findFirst({ where: { id: metadata.data.organisationId, }, select: { name: true, url: true, avatarImageId: true, }, }); if (!organisation) { throw data({ type: 'invalid-token', }); } return { token, type: metadata.data.type, user: { name: verificationToken.user.name, email: verificationToken.user.email, avatar: verificationToken.user.avatarImageId, }, organisation: { name: organisation.name, url: organisation.url, avatar: organisation.avatarImageId, }, } as const; } export default function OrganisationSsoConfirmationTokenPage({ loaderData }: Route.ComponentProps) { const { token, type, user, organisation } = loaderData; const { toast } = useToast(); const navigate = useNavigate(); const [isConfirmationChecked, setIsConfirmationChecked] = useState(false); const { mutate: declineLinkOrganisationAccount, isPending: isDeclining } = trpc.enterprise.organisation.authenticationPortal.declineLinkAccount.useMutation({ onSuccess: async () => { await navigate('/'); toast({ title: 'Account link declined', }); }, onError: (error) => { toast({ title: 'Error declining account link', description: error.message, }); }, }); const { mutate: linkOrganisationAccount, isPending: isLinking } = trpc.enterprise.organisation.authenticationPortal.linkAccount.useMutation({ onSuccess: async () => { await navigate(formatOrganisationLoginPath(organisation.url)); toast({ title: 'Account linked successfully', }); }, onError: (error) => { toast({ title: 'Error linking account', description: error.message, }); }, }); return (
{type === 'link' ? ( Account Linking Request ) : ( Account Creation Request )} {type === 'link' ? ( An organisation wants to link your account. Please review the details below. ) : ( An organisation wants to create an account for you. Please review the details below. )} {/* Current User Section */}

Your Account

Account
{/* Organisation Section */}

Requesting Organisation

Organisation
{/* Warnings Section */}

Important: What This Means

By accepting this request, you grant {organisation.name} the following permissions:

  • Full account access: {' '} View all your profile information, settings, and activity
  • Account management: {' '} Modify your account settings, permissions, and preferences
  • Data access:{' '} Access all data associated with your account
This organisation will have administrative control over your account. You can revoke this access later, but they will retain access to any data they've already collected.
setIsConfirmationChecked(checked === 'indeterminate' ? false : checked) } />
); }