diff --git a/.env.example b/.env.example index 9250ab9cf..f21560ca3 100644 --- a/.env.example +++ b/.env.example @@ -25,7 +25,7 @@ NEXT_PRIVATE_DIRECT_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/ # [[E2E Tests]] E2E_TEST_AUTHENTICATE_USERNAME="Test User" E2E_TEST_AUTHENTICATE_USER_EMAIL="testuser@mail.com" -E2E_TEST_AUTHENTICATE_USER_PASSWORD="test_password" +E2E_TEST_AUTHENTICATE_USER_PASSWORD="test_Password123" # [[STORAGE]] # OPTIONAL: Defines the storage transport to use. Available options: database (default) | s3 diff --git a/apps/web/src/components/forms/password.tsx b/apps/web/src/components/forms/password.tsx index 0eb491537..0fa5ad462 100644 --- a/apps/web/src/components/forms/password.tsx +++ b/apps/web/src/components/forms/password.tsx @@ -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', diff --git a/apps/web/src/components/forms/reset-password.tsx b/apps/web/src/components/forms/reset-password.tsx index 354584f6e..03608a27d 100644 --- a/apps/web/src/components/forms/reset-password.tsx +++ b/apps/web/src/components/forms/reset-password.tsx @@ -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'], diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx index 038f9fe68..17bb2c57c 100644 --- a/apps/web/src/components/forms/signin.tsx +++ b/apps/web/src/components/forms/signin.tsx @@ -9,6 +9,7 @@ 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'; @@ -39,7 +40,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(), }); diff --git a/apps/web/src/components/forms/signup.tsx b/apps/web/src/components/forms/signup.tsx index 3f2723ec8..ebfbf72c9 100644 --- a/apps/web/src/components/forms/signup.tsx +++ b/apps/web/src/components/forms/signup.tsx @@ -9,6 +9,7 @@ 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 { @@ -26,15 +27,22 @@ import { useToast } from '@documenso/ui/primitives/use-toast'; 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: 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' }), -}); +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; diff --git a/packages/trpc/server/auth-router/schema.ts b/packages/trpc/server/auth-router/schema.ts index cc969c679..dbe42a25c 100644 --- a/packages/trpc/server/auth-router/schema.ts +++ b/packages/trpc/server/auth-router/schema.ts @@ -1,9 +1,25 @@ import { z } from 'zod'; +export const ZCurrentPasswordSchema = z + .string() + .min(6, { message: 'Must be at least 6 characters in length' }) + .max(72); + +export const ZPasswordSchema = z + .string() + .regex(new RegExp('.*[A-Z].*'), { message: 'One uppercase character' }) + .regex(new RegExp('.*[a-z].*'), { message: 'One lowercase character' }) + .regex(new RegExp('.*\\d.*'), { message: 'One number' }) + .regex(new RegExp('.*[`~<>?,./!@#$%^&*()\\-_+="\'|{}\\[\\];:\\\\].*'), { + message: 'One special character is required', + }) + .min(8, { message: 'Must be at least 8 characters in length' }) + .max(72, { message: 'Cannot be more than 72 characters in length' }); + export const ZSignUpMutationSchema = z.object({ name: z.string().min(1), email: z.string().email(), - password: z.string().min(6), + password: ZPasswordSchema, signature: z.string().min(1, { message: 'A signature is required.' }), }); diff --git a/packages/trpc/server/profile-router/schema.ts b/packages/trpc/server/profile-router/schema.ts index ef9ca2a14..1d6820007 100644 --- a/packages/trpc/server/profile-router/schema.ts +++ b/packages/trpc/server/profile-router/schema.ts @@ -1,5 +1,7 @@ import { z } from 'zod'; +import { ZCurrentPasswordSchema, ZPasswordSchema } from '../auth-router/schema'; + export const ZRetrieveUserByIdQuerySchema = z.object({ id: z.number().min(1), }); @@ -10,8 +12,8 @@ export const ZUpdateProfileMutationSchema = z.object({ }); export const ZUpdatePasswordMutationSchema = z.object({ - currentPassword: z.string().min(6), - password: z.string().min(6), + currentPassword: ZCurrentPasswordSchema, + password: ZPasswordSchema, }); export const ZForgotPasswordFormSchema = z.object({ @@ -19,7 +21,7 @@ export const ZForgotPasswordFormSchema = z.object({ }); export const ZResetPasswordFormSchema = z.object({ - password: z.string().min(6), + password: ZPasswordSchema, token: z.string().min(1), }); diff --git a/packages/ui/primitives/data-table-pagination.tsx b/packages/ui/primitives/data-table-pagination.tsx index 8147c92fb..feebf6c54 100644 --- a/packages/ui/primitives/data-table-pagination.tsx +++ b/packages/ui/primitives/data-table-pagination.tsx @@ -1,4 +1,4 @@ -import { Table } from '@tanstack/react-table'; +import type { Table } from '@tanstack/react-table'; import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react'; import { match } from 'ts-pattern';