diff --git a/apps/web/src/app/(unauthenticated)/signin/page.tsx b/apps/web/src/app/(unauthenticated)/signin/page.tsx
index 5fda07e70..1332a3f37 100644
--- a/apps/web/src/app/(unauthenticated)/signin/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/signin/page.tsx
@@ -1,9 +1,14 @@
+import type { Metadata } from 'next';
import Link from 'next/link';
import { IS_GOOGLE_SSO_ENABLED } from '@documenso/lib/constants/auth';
import { SignInForm } from '~/components/forms/signin';
+export const metadata: Metadata = {
+ title: 'Sign In',
+};
+
export default function SignInPage() {
return (
diff --git a/apps/web/src/app/(unauthenticated)/signup/page.tsx b/apps/web/src/app/(unauthenticated)/signup/page.tsx
index 05b9caf21..c6d49f891 100644
--- a/apps/web/src/app/(unauthenticated)/signup/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/signup/page.tsx
@@ -1,3 +1,4 @@
+import type { Metadata } from 'next';
import Link from 'next/link';
import { redirect } from 'next/navigation';
@@ -5,6 +6,10 @@ import { IS_GOOGLE_SSO_ENABLED } from '@documenso/lib/constants/auth';
import { SignUpForm } from '~/components/forms/signup';
+export const metadata: Metadata = {
+ title: 'Sign Up',
+};
+
export default function SignUpPage() {
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === 'true') {
redirect('/signin');
diff --git a/apps/web/src/app/(unauthenticated)/verify-email/page.tsx b/apps/web/src/app/(unauthenticated)/verify-email/page.tsx
index 04202d19b..30d2baf16 100644
--- a/apps/web/src/app/(unauthenticated)/verify-email/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/verify-email/page.tsx
@@ -1,9 +1,14 @@
+import type { Metadata } from 'next';
import Link from 'next/link';
import { XCircle } from 'lucide-react';
import { Button } from '@documenso/ui/primitives/button';
+export const metadata: Metadata = {
+ title: 'Verify Email',
+};
+
export default function EmailVerificationWithoutTokenPage() {
return (
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index ac88469b0..17f92fa2b 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -20,7 +20,10 @@ const fontInter = Inter({ subsets: ['latin'], variable: '--font-sans' });
const fontCaveat = Caveat({ subsets: ['latin'], variable: '--font-signature' });
export const metadata = {
- title: 'Documenso - The Open Source DocuSign Alternative',
+ title: {
+ template: '%s - Documenso',
+ default: 'Documenso',
+ },
description:
'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
keywords:
diff --git a/apps/web/src/components/(dashboard)/layout/header.tsx b/apps/web/src/components/(dashboard)/layout/header.tsx
index bdae6c511..ba35671e6 100644
--- a/apps/web/src/components/(dashboard)/layout/header.tsx
+++ b/apps/web/src/components/(dashboard)/layout/header.tsx
@@ -33,7 +33,7 @@ export const Header = ({ className, user, ...props }: HeaderProps) => {
return (
5 && 'border-b-border',
className,
)}
diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx
index 252432b89..f2432c071 100644
--- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx
+++ b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx
@@ -68,7 +68,7 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
-
+
Account
{isUserAdmin && (
@@ -122,7 +122,7 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
Themes
-
+
Light
diff --git a/apps/web/src/components/(dashboard)/period-selector/types.ts b/apps/web/src/components/(dashboard)/period-selector/types.ts
index 2b50f5d6c..8ae1c5fbe 100644
--- a/apps/web/src/components/(dashboard)/period-selector/types.ts
+++ b/apps/web/src/components/(dashboard)/period-selector/types.ts
@@ -1,4 +1,4 @@
-export type PeriodSelectorValue = '' | '7d' | '14d' | '30d';
+import type { PeriodSelectorValue } from '@documenso/lib/server-only/document/find-documents';
export const isPeriodSelectorValue = (value: unknown): value is PeriodSelectorValue => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
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/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts
index def85f2d4..2929c515b 100644
--- a/packages/lib/server-only/document/find-documents.ts
+++ b/packages/lib/server-only/document/find-documents.ts
@@ -9,6 +9,8 @@ import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-documen
import type { FindResultSet } from '../../types/find-result-set';
import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-tokens-for-document';
+export type PeriodSelectorValue = '' | '7d' | '14d' | '30d';
+
export type FindDocumentsOptions = {
userId: number;
term?: string;
@@ -19,7 +21,7 @@ export type FindDocumentsOptions = {
column: keyof Omit;
direction: 'asc' | 'desc';
};
- period?: '' | '7d' | '14d' | '30d';
+ period?: PeriodSelectorValue;
};
export const findDocuments = async ({
diff --git a/packages/lib/server-only/document/get-stats.ts b/packages/lib/server-only/document/get-stats.ts
index 044d9a2dc..6aaa9a596 100644
--- a/packages/lib/server-only/document/get-stats.ts
+++ b/packages/lib/server-only/document/get-stats.ts
@@ -1,14 +1,31 @@
+import { DateTime } from 'luxon';
+
import { prisma } from '@documenso/prisma';
-import type { User } from '@documenso/prisma/client';
+import type { Prisma, User } from '@documenso/prisma/client';
import { SigningStatus } from '@documenso/prisma/client';
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
+import type { PeriodSelectorValue } from './find-documents';
+
export type GetStatsInput = {
user: User;
+ period?: PeriodSelectorValue;
};
-export const getStats = async ({ user }: GetStatsInput) => {
+export const getStats = async ({ user, period }: GetStatsInput) => {
+ let createdAt: Prisma.DocumentWhereInput['createdAt'];
+
+ if (period) {
+ const daysAgo = parseInt(period.replace(/d$/, ''), 10);
+
+ const startOfPeriod = DateTime.now().minus({ days: daysAgo }).startOf('day');
+
+ createdAt = {
+ gte: startOfPeriod.toJSDate(),
+ };
+ }
+
const [ownerCounts, notSignedCounts, hasSignedCounts] = await Promise.all([
prisma.document.groupBy({
by: ['status'],
@@ -17,6 +34,7 @@ export const getStats = async ({ user }: GetStatsInput) => {
},
where: {
userId: user.id,
+ createdAt,
deletedAt: null,
},
}),
@@ -33,6 +51,7 @@ export const getStats = async ({ user }: GetStatsInput) => {
signingStatus: SigningStatus.NOT_SIGNED,
},
},
+ createdAt,
deletedAt: null,
},
}),
@@ -42,6 +61,7 @@ export const getStats = async ({ user }: GetStatsInput) => {
_all: true,
},
where: {
+ createdAt,
User: {
email: {
not: user.email,
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';