Skip to content

Commit

Permalink
Merge pull request #140 from documenso/main
Browse files Browse the repository at this point in the history
feat: add passkey support
  • Loading branch information
SergeWilfried authored Apr 1, 2024
2 parents 7cadb2d + cbe6270 commit bdf653d
Show file tree
Hide file tree
Showing 35 changed files with 973 additions and 255 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const UsersDataTable = ({
perPage,
});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedSearchString]);

const onPaginationChange = (page: number, perPage: number) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';

export type CreatePasskeyDialogProps = {
trigger?: React.ReactNode;
onSuccess?: () => void;
} & Omit<DialogPrimitive.DialogProps, 'children'>;

const ZCreatePasskeyFormSchema = z.object({
Expand All @@ -48,7 +49,7 @@ type TCreatePasskeyFormSchema = z.infer<typeof ZCreatePasskeyFormSchema>;

const parser = new UAParser();

export const CreatePasskeyDialog = ({ trigger, ...props }: CreatePasskeyDialogProps) => {
export const CreatePasskeyDialog = ({ trigger, onSuccess, ...props }: CreatePasskeyDialogProps) => {
const [open, setOpen] = useState(false);
const [formError, setFormError] = useState<string | null>(null);

Expand Down Expand Up @@ -84,6 +85,7 @@ export const CreatePasskeyDialog = ({ trigger, ...props }: CreatePasskeyDialogPr
duration: 5000,
});

onSuccess?.();
setOpen(false);
} catch (err) {
if (err.name === 'NotAllowedError') {
Expand Down
172 changes: 172 additions & 0 deletions apps/web/src/app/(signing)/sign/[token]/document-action-auth-2fa.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { useEffect, useState } from 'react';

import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';

import { AppError } from '@documenso/lib/errors/app-error';
import { DocumentAuth, type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { RecipientRole } from '@documenso/prisma/client';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import { DialogFooter } from '@documenso/ui/primitives/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';

import { EnableAuthenticatorAppDialog } from '~/components/forms/2fa/enable-authenticator-app-dialog';

import { useRequiredDocumentAuthContext } from './document-auth-provider';

export type DocumentActionAuth2FAProps = {
actionTarget?: 'FIELD' | 'DOCUMENT';
actionVerb?: string;
open: boolean;
onOpenChange: (value: boolean) => void;
onReauthFormSubmit: (values?: TRecipientActionAuth) => Promise<void> | void;
};

const Z2FAAuthFormSchema = z.object({
token: z
.string()
.min(4, { message: 'Token must at least 4 characters long' })
.max(10, { message: 'Token must be at most 10 characters long' }),
});

type T2FAAuthFormSchema = z.infer<typeof Z2FAAuthFormSchema>;

export const DocumentActionAuth2FA = ({
actionTarget = 'FIELD',
actionVerb = 'sign',
onReauthFormSubmit,
open,
onOpenChange,
}: DocumentActionAuth2FAProps) => {
const { recipient, user, isCurrentlyAuthenticating, setIsCurrentlyAuthenticating } =
useRequiredDocumentAuthContext();

const form = useForm<T2FAAuthFormSchema>({
resolver: zodResolver(Z2FAAuthFormSchema),
defaultValues: {
token: '',
},
});

const [is2FASetupSuccessful, setIs2FASetupSuccessful] = useState(false);
const [formErrorCode, setFormErrorCode] = useState<string | null>(null);

const onFormSubmit = async ({ token }: T2FAAuthFormSchema) => {
try {
setIsCurrentlyAuthenticating(true);

await onReauthFormSubmit({
type: DocumentAuth.TWO_FACTOR_AUTH,
token,
});

setIsCurrentlyAuthenticating(false);

onOpenChange(false);
} catch (err) {
setIsCurrentlyAuthenticating(false);

const error = AppError.parseError(err);
setFormErrorCode(error.code);

// Todo: Alert.
}
};

useEffect(() => {
form.reset({
token: '',
});

setIs2FASetupSuccessful(false);
setFormErrorCode(null);

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]);

if (!user?.twoFactorEnabled && !is2FASetupSuccessful) {
return (
<div className="space-y-4">
<Alert variant="warning">
<AlertDescription>
<p>
{recipient.role === RecipientRole.VIEWER && actionTarget === 'DOCUMENT'
? 'You need to setup 2FA to mark this document as viewed.'
: `You need to setup 2FA to ${actionVerb.toLowerCase()} this ${actionTarget.toLowerCase()}.`}
</p>

{user?.identityProvider === 'DOCUMENSO' && (
<p className="mt-2">
By enabling 2FA, you will be required to enter a code from your authenticator app
every time you sign in.
</p>
)}
</AlertDescription>
</Alert>

<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
Close
</Button>

<EnableAuthenticatorAppDialog onSuccess={() => setIs2FASetupSuccessful(true)} />
</DialogFooter>
</div>
);
}

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onFormSubmit)}>
<fieldset disabled={isCurrentlyAuthenticating}>
<div className="space-y-4">
<FormField
control={form.control}
name="token"
render={({ field }) => (
<FormItem>
<FormLabel required>2FA token</FormLabel>

<FormControl>
<Input {...field} placeholder="Token" />
</FormControl>

<FormMessage />
</FormItem>
)}
/>

{formErrorCode && (
<Alert variant="destructive">
<AlertTitle>Unauthorized</AlertTitle>
<AlertDescription>
We were unable to verify your details. Please try again or contact support
</AlertDescription>
</Alert>
)}

<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
Cancel
</Button>

<Button type="submit" loading={isCurrentlyAuthenticating}>
Sign
</Button>
</DialogFooter>
</div>
</fieldset>
</form>
</Form>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useState } from 'react';

import { DateTime } from 'luxon';
import { signOut } from 'next-auth/react';

import { RecipientRole } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import { DialogFooter } from '@documenso/ui/primitives/dialog';

import { useRequiredDocumentAuthContext } from './document-auth-provider';

export type DocumentActionAuthAccountProps = {
actionTarget?: 'FIELD' | 'DOCUMENT';
actionVerb?: string;
onOpenChange: (value: boolean) => void;
};

export const DocumentActionAuthAccount = ({
actionTarget = 'FIELD',
actionVerb = 'sign',
onOpenChange,
}: DocumentActionAuthAccountProps) => {
const { recipient } = useRequiredDocumentAuthContext();

const [isSigningOut, setIsSigningOut] = useState(false);

const { mutateAsync: encryptSecondaryData } = trpc.crypto.encryptSecondaryData.useMutation();

const handleChangeAccount = async (email: string) => {
try {
setIsSigningOut(true);

const encryptedEmail = await encryptSecondaryData({
data: email,
expiresAt: DateTime.now().plus({ days: 1 }).toMillis(),
});

await signOut({
callbackUrl: `/signin?email=${encodeURIComponent(encryptedEmail)}`,
});
} catch {
setIsSigningOut(false);

// Todo: Alert.
}
};

return (
<fieldset disabled={isSigningOut} className="space-y-4">
<Alert variant="warning">
<AlertDescription>
{actionTarget === 'DOCUMENT' && recipient.role === RecipientRole.VIEWER ? (
<span>
To mark this document as viewed, you need to be logged in as{' '}
<strong>{recipient.email}</strong>
</span>
) : (
<span>
To {actionVerb.toLowerCase()} this {actionTarget.toLowerCase()}, you need to be logged
in as <strong>{recipient.email}</strong>
</span>
)}
</AlertDescription>
</Alert>

<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
Cancel
</Button>

<Button onClick={async () => handleChangeAccount(recipient.email)} loading={isSigningOut}>
Login
</Button>
</DialogFooter>
</fieldset>
);
};
Loading

0 comments on commit bdf653d

Please sign in to comment.