Merge branch 'main' into feat/account-deletion

This commit is contained in:
Ephraim Duncan
2024-02-05 13:01:59 +00:00
committed by GitHub
125 changed files with 2076 additions and 439 deletions

View File

@ -19,27 +19,14 @@ export const AuthenticatorApp = ({ isTwoFactorEnabled }: AuthenticatorAppProps)
return (
<>
<div className="mt-4 flex flex-col justify-between gap-4 rounded-lg border p-4 md:flex-row md:items-center md:gap-8">
<div className="flex-1">
<p>Authenticator app</p>
<p className="text-muted-foreground mt-2 max-w-[50ch] text-sm">
Create one-time passwords that serve as a secondary authentication method for confirming
your identity when requested during the sign-in process.
</p>
</div>
<div>
{isTwoFactorEnabled ? (
<Button variant="destructive" onClick={() => setModalState('disable')} size="sm">
Disable 2FA
</Button>
) : (
<Button onClick={() => setModalState('enable')} size="sm">
Enable 2FA
</Button>
)}
</div>
<div className="flex-shrink-0">
{isTwoFactorEnabled ? (
<Button variant="destructive" onClick={() => setModalState('disable')}>
Disable 2FA
</Button>
) : (
<Button onClick={() => setModalState('enable')}>Enable 2FA</Button>
)}
</div>
<EnableAuthenticatorAppDialog

View File

@ -11,6 +11,7 @@ import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
@ -145,8 +146,8 @@ export const DisableAuthenticatorAppDialog = ({
/>
</fieldset>
<div className="flex w-full items-center justify-between">
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
Cancel
</Button>
@ -157,7 +158,7 @@ export const DisableAuthenticatorAppDialog = ({
>
Disable 2FA
</Button>
</div>
</DialogFooter>
</form>
</Form>
</DialogContent>

View File

@ -15,6 +15,7 @@ import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
@ -190,15 +191,15 @@ export const EnableAuthenticatorAppDialog = ({
)}
/>
<div className="flex w-full items-center justify-between">
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button type="submit" loading={isSetupTwoFactorAuthenticationSubmitting}>
Continue
</Button>
</div>
</DialogFooter>
</form>
</Form>
);
@ -251,15 +252,15 @@ export const EnableAuthenticatorAppDialog = ({
)}
/>
<div className="flex w-full items-center justify-between">
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button type="submit" loading={isEnableTwoFactorAuthenticationSubmitting}>
Enable 2FA
</Button>
</div>
</DialogFooter>
</form>
</Form>
))

View File

@ -7,7 +7,6 @@ import { Button } from '@documenso/ui/primitives/button';
import { ViewRecoveryCodesDialog } from './view-recovery-codes-dialog';
type RecoveryCodesProps = {
// backupCodes: string[] | null;
isTwoFactorEnabled: boolean;
};
@ -16,22 +15,13 @@ export const RecoveryCodes = ({ isTwoFactorEnabled }: RecoveryCodesProps) => {
return (
<>
<div className="mt-4 flex flex-col justify-between gap-4 rounded-lg border p-4 md:flex-row md:items-center md:gap-8">
<div className="flex-1">
<p>Recovery Codes</p>
<p className="text-muted-foreground mt-2 max-w-[50ch] text-sm">
Recovery codes are used to access your account in the event that you lose access to your
authenticator app.
</p>
</div>
<div>
<Button onClick={() => setIsOpen(true)} disabled={!isTwoFactorEnabled} size="sm">
View Codes
</Button>
</div>
</div>
<Button
className="flex-shrink-0"
onClick={() => setIsOpen(true)}
disabled={!isTwoFactorEnabled}
>
View Codes
</Button>
<ViewRecoveryCodesDialog
key={isOpen ? 'open' : 'closed'}

View File

@ -11,6 +11,7 @@ import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
@ -119,15 +120,15 @@ export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCode
)}
/>
<div className="flex w-full items-center justify-between">
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button type="submit" loading={isViewRecoveryCodesSubmitting}>
Continue
</Button>
</div>
</DialogFooter>
</form>
</Form>
);

View File

@ -7,6 +7,7 @@ import { z } from 'zod';
import type { User } from '@documenso/prisma/client';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react';
import { ZCurrentPasswordSchema, ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import {
@ -22,18 +23,9 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
export const ZPasswordFormSchema = z
.object({
currentPassword: z
.string()
.min(6, { message: 'Password should contain at least 6 characters' })
.max(72, { message: 'Password should not contain more than 72 characters' }),
password: z
.string()
.min(6, { message: 'Password should contain at least 6 characters' })
.max(72, { message: 'Password should not contain more than 72 characters' }),
repeatedPassword: z
.string()
.min(6, { message: 'Password should contain at least 6 characters' })
.max(72, { message: 'Password should not contain more than 72 characters' }),
currentPassword: ZCurrentPasswordSchema,
password: ZPasswordSchema,
repeatedPassword: ZPasswordSchema,
})
.refine((data) => data.password === data.repeatedPassword, {
message: 'Passwords do not match',
@ -145,7 +137,7 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
/>
</fieldset>
<div className="mt-4">
<div className="ml-auto mt-4">
<Button type="submit" loading={isSubmitting}>
{isSubmitting ? 'Updating password...' : 'Update password'}
</Button>

View File

@ -197,7 +197,6 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
</Label>
<Input id="email" type="email" className="bg-muted mt-2" value={user.email} disabled />
</div>
<FormField
control={form.control}
name="signature"
@ -207,7 +206,8 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
<FormControl>
<SignaturePad
className="h-44 w-full"
containerClassName="rounded-lg border bg-background"
disabled={isSubmitting}
containerClassName={cn('rounded-lg border bg-background')}
defaultValue={user.signature ?? undefined}
onChange={(v) => onChange(v ?? '')}
/>

View File

@ -8,6 +8,7 @@ import { z } from 'zod';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react';
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import {
@ -23,8 +24,8 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
export const ZResetPasswordFormSchema = z
.object({
password: z.string().min(6).max(72),
repeatedPassword: z.string().min(6).max(72),
password: ZPasswordSchema,
repeatedPassword: ZPasswordSchema,
})
.refine((data) => data.password === data.repeatedPassword, {
path: ['repeatedPassword'],

View File

@ -9,9 +9,16 @@ import { FcGoogle } from 'react-icons/fc';
import { z } from 'zod';
import { ErrorCode, isErrorCode } from '@documenso/lib/next-auth/error-codes';
import { ZCurrentPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@documenso/ui/primitives/dialog';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import {
Form,
FormControl,
@ -39,7 +46,7 @@ const LOGIN_REDIRECT_PATH = '/documents';
export const ZSignInFormSchema = z.object({
email: z.string().email().min(1),
password: z.string().min(6).max(72),
password: ZCurrentPasswordSchema,
totpCode: z.string().trim().optional(),
backupCode: z.string().trim().optional(),
});
@ -48,9 +55,10 @@ export type TSignInFormSchema = z.infer<typeof ZSignInFormSchema>;
export type SignInFormProps = {
className?: string;
isGoogleSSOEnabled?: boolean;
};
export const SignInForm = ({ className }: SignInFormProps) => {
export const SignInForm = ({ className, isGoogleSSOEnabled }: SignInFormProps) => {
const { toast } = useToast();
const [isTwoFactorAuthenticationDialogOpen, setIsTwoFactorAuthenticationDialogOpen] =
useState(false);
@ -109,7 +117,6 @@ export const SignInForm = ({ className }: SignInFormProps) => {
const result = await signIn('credentials', {
...credentials,
callbackUrl: LOGIN_REDIRECT_PATH,
redirect: false,
});
@ -203,24 +210,29 @@ export const SignInForm = ({ className }: SignInFormProps) => {
{isSubmitting ? 'Signing in...' : 'Sign In'}
</Button>
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
<div className="bg-border h-px flex-1" />
<span className="text-muted-foreground bg-transparent">Or continue with</span>
<div className="bg-border h-px flex-1" />
</div>
{isGoogleSSOEnabled && (
<>
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
<div className="bg-border h-px flex-1" />
<span className="text-muted-foreground bg-transparent">Or continue with</span>
<div className="bg-border h-px flex-1" />
</div>
<Button
type="button"
size="lg"
variant={'outline'}
className="bg-background text-muted-foreground border"
disabled={isSubmitting}
onClick={onSignInWithGoogleClick}
>
<FcGoogle className="mr-2 h-5 w-5" />
Google
</Button>
<Button
type="button"
size="lg"
variant="outline"
className="bg-background text-muted-foreground border"
disabled={isSubmitting}
onClick={onSignInWithGoogleClick}
>
<FcGoogle className="mr-2 h-5 w-5" />
Google
</Button>
</>
)}
</form>
<Dialog
open={isTwoFactorAuthenticationDialogOpen}
onOpenChange={onCloseTwoFactorAuthenticationDialog}
@ -263,21 +275,23 @@ export const SignInForm = ({ className }: SignInFormProps) => {
)}
/>
)}
<DialogFooter className="mt-4">
<Button
type="button"
variant="secondary"
onClick={onToggleTwoFactorAuthenticationMethodClick}
>
{twoFactorAuthenticationMethod === 'totp'
? 'Use Backup Code'
: 'Use Authenticator'}
</Button>
<Button type="submit" loading={isSubmitting}>
{isSubmitting ? 'Signing in...' : 'Sign In'}
</Button>
</DialogFooter>
</fieldset>
<div className="mt-4 flex items-center justify-between">
<Button
type="button"
variant="ghost"
onClick={onToggleTwoFactorAuthenticationMethodClick}
>
{twoFactorAuthenticationMethod === 'totp' ? 'Use Backup Code' : 'Use Authenticator'}
</Button>
<Button type="submit" loading={isSubmitting}>
{isSubmitting ? 'Signing in...' : 'Sign In'}
</Button>
</div>
</form>
</DialogContent>
</Dialog>

View File

@ -3,11 +3,13 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { signIn } from 'next-auth/react';
import { useForm } from 'react-hook-form';
import { FcGoogle } from 'react-icons/fc';
import { z } from 'zod';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react';
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import {
@ -23,23 +25,33 @@ import { PasswordInput } from '@documenso/ui/primitives/password-input';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useToast } from '@documenso/ui/primitives/use-toast';
export const ZSignUpFormSchema = z.object({
name: z.string().trim().min(1, { message: 'Please enter a valid name.' }),
email: z.string().email().min(1),
password: z
.string()
.min(6, { message: 'Password should contain at least 6 characters' })
.max(72, { message: 'Password should not contain more than 72 characters' }),
signature: z.string().min(1, { message: 'We need your signature to sign documents' }),
});
const SIGN_UP_REDIRECT_PATH = '/documents';
export const ZSignUpFormSchema = z
.object({
name: z.string().trim().min(1, { message: 'Please enter a valid name.' }),
email: z.string().email().min(1),
password: ZPasswordSchema,
signature: z.string().min(1, { message: 'We need your signature to sign documents' }),
})
.refine(
(data) => {
const { name, email, password } = data;
return !password.includes(name) && !password.includes(email.split('@')[0]);
},
{
message: 'Password should not be common or based on personal information',
},
);
export type TSignUpFormSchema = z.infer<typeof ZSignUpFormSchema>;
export type SignUpFormProps = {
className?: string;
isGoogleSSOEnabled?: boolean;
};
export const SignUpForm = ({ className }: SignUpFormProps) => {
export const SignUpForm = ({ className, isGoogleSSOEnabled }: SignUpFormProps) => {
const { toast } = useToast();
const analytics = useAnalytics();
@ -64,7 +76,7 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
await signIn('credentials', {
email,
password,
callbackUrl: '/',
callbackUrl: SIGN_UP_REDIRECT_PATH,
});
analytics.capture('App: User Sign Up', {
@ -89,6 +101,19 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
}
};
const onSignUpWithGoogleClick = async () => {
try {
await signIn('google', { callbackUrl: SIGN_UP_REDIRECT_PATH });
} catch (err) {
toast({
title: 'An unknown error occurred',
description:
'We encountered an unknown error while attempting to sign you Up. Please try again later.',
variant: 'destructive',
});
}
};
return (
<Form {...form}>
<form
@ -147,6 +172,7 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
<FormControl>
<SignaturePad
className="h-36 w-full"
disabled={isSubmitting}
containerClassName="mt-2 rounded-lg border bg-background"
onChange={(v) => onChange(v ?? '')}
/>
@ -166,6 +192,28 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
>
{isSubmitting ? 'Signing up...' : 'Sign Up'}
</Button>
{isGoogleSSOEnabled && (
<>
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
<div className="bg-border h-px flex-1" />
<span className="text-muted-foreground bg-transparent">Or</span>
<div className="bg-border h-px flex-1" />
</div>
<Button
type="button"
size="lg"
variant={'outline'}
className="bg-background text-muted-foreground border"
disabled={isSubmitting}
onClick={onSignUpWithGoogleClick}
>
<FcGoogle className="mr-2 h-5 w-5" />
Sign Up with Google
</Button>
</>
)}
</form>
</Form>
);