This commit is contained in:
David Nguyen
2025-01-31 14:09:02 +11:00
parent f7a98180d7
commit d7d0fca501
146 changed files with 1250 additions and 1263 deletions

View File

@ -1,21 +0,0 @@
import { redirect, useParams } from 'react-router';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { useOptionalCurrentTeam } from '~/providers/team';
import { DocumentEditPageView } from './document-edit-page-view';
export default function DocumentEditPage() {
const { id: documentId } = useParams();
const team = useOptionalCurrentTeam();
const documentRootPath = formatDocumentsPath(team?.url);
if (!documentId || Number.isNaN(documentId)) {
redirect(documentRootPath);
}
return <DocumentEditPageView documentId={Number(documentId)} />;
}

View File

@ -1,21 +0,0 @@
import { redirect, useParams } from 'react-router';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { useOptionalCurrentTeam } from '~/providers/team';
import { DocumentPageView } from './document-page-view';
export default function DocumentPage() {
const { id: documentId } = useParams();
const team = useOptionalCurrentTeam();
const documentRootPath = formatDocumentsPath(team?.url);
if (!documentId || Number.isNaN(documentId)) {
redirect(documentRootPath);
}
return <DocumentPageView documentId={Number(documentId)} />;
}

View File

@ -1,21 +0,0 @@
import { redirect, useParams } from 'react-router';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { useOptionalCurrentTeam } from '~/providers/team';
import { DocumentLogsPageView } from './document-logs-page-view';
export default function DocumentsLogsPage() {
const { id: documentId } = useParams();
const team = useOptionalCurrentTeam();
const documentRootPath = formatDocumentsPath(team?.url);
if (!documentId || Number.isNaN(documentId)) {
redirect(documentRootPath);
}
return <DocumentLogsPageView documentId={Number(documentId)} />;
}

View File

@ -25,8 +25,7 @@ export const DocumentSearch = ({ initialValue = '' }: { initialValue?: string })
params.delete('search'); params.delete('search');
} }
// Todo: Test void navigate(`?${params.toString()}`);
void navigate(`/documents?${params.toString()}`);
}, },
[searchParams], [searchParams],
); );

View File

@ -20,7 +20,7 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { Skeleton } from '@documenso/ui/primitives/skeleton'; import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table'; import { TableCell } from '@documenso/ui/primitives/table';
import { LeaveTeamDialog } from '../dialogs/leave-team-dialog'; import { TeamLeaveDialog } from '~/components/dialogs/team-leave-dialog';
export const CurrentUserTeamsDataTable = () => { export const CurrentUserTeamsDataTable = () => {
const { _, i18n } = useLingui(); const { _, i18n } = useLingui();
@ -99,7 +99,7 @@ export const CurrentUserTeamsDataTable = () => {
</Button> </Button>
)} )}
<LeaveTeamDialog <TeamLeaveDialog
teamId={row.original.id} teamId={row.original.id}
teamName={row.original.name} teamName={row.original.name}
teamAvatarImageId={row.original.avatarImageId} teamAvatarImageId={row.original.avatarImageId}

View File

@ -15,7 +15,8 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { Skeleton } from '@documenso/ui/primitives/skeleton'; import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table'; import { TableCell } from '@documenso/ui/primitives/table';
import { CreateTeamCheckoutDialog } from '../dialogs/create-team-checkout-dialog'; import { TeamCheckoutCreateDialog } from '~/components/dialogs/team-checkout-create-dialog';
import { PendingUserTeamsDataTableActions } from './pending-user-teams-data-table-actions'; import { PendingUserTeamsDataTableActions } from './pending-user-teams-data-table-actions';
export const PendingUserTeamsDataTable = () => { export const PendingUserTeamsDataTable = () => {
@ -139,7 +140,7 @@ export const PendingUserTeamsDataTable = () => {
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />} {(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
</DataTable> </DataTable>
<CreateTeamCheckoutDialog <TeamCheckoutCreateDialog
pendingTeamId={checkoutPendingTeamId} pendingTeamId={checkoutPendingTeamId}
onClose={() => setCheckoutPendingTeamId(null)} onClose={() => setCheckoutPendingTeamId(null)}
/> />

View File

@ -26,8 +26,8 @@ import {
import { Skeleton } from '@documenso/ui/primitives/skeleton'; import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table'; import { TableCell } from '@documenso/ui/primitives/table';
import { DeleteTeamMemberDialog } from '../dialogs/delete-team-member-dialog'; import { DeleteTeamMemberDialog } from '../../dialogs/team-member-delete-dialog';
import { UpdateTeamMemberDialog } from '../dialogs/update-team-member-dialog'; import { UpdateTeamMemberDialog } from '../../dialogs/team-member-update-dialog';
export type TeamMembersDataTableProps = { export type TeamMembersDataTableProps = {
currentUserTeamRole: TeamMemberRole; currentUserTeamRole: TeamMemberRole;

View File

@ -0,0 +1,155 @@
import { useState } from 'react';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { authClient } from '@documenso/auth/client';
import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useAuth } from '~/providers/auth';
export type AccountDeleteDialogProps = {
className?: string;
};
export const AccountDeleteDialog = ({ className }: AccountDeleteDialogProps) => {
const { user } = useAuth();
const { _ } = useLingui();
const { toast } = useToast();
const hasTwoFactorAuthentication = user.twoFactorEnabled;
const [enteredEmail, setEnteredEmail] = useState<string>('');
const { mutateAsync: deleteAccount, isPending: isDeletingAccount } =
trpc.profile.deleteAccount.useMutation();
const onDeleteAccount = async () => {
try {
await deleteAccount();
toast({
title: _(msg`Account deleted`),
description: _(msg`Your account has been deleted successfully.`),
duration: 5000,
});
return await authClient.signOut();
} catch (err) {
toast({
title: _(msg`An unknown error occurred`),
variant: 'destructive',
description: _(
msg`We encountered an unknown error while attempting to delete your account. Please try again later.`,
),
});
}
};
return (
<div className={className}>
<Alert
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral"
>
<div>
<AlertTitle>
<Trans>Delete Account</Trans>
</AlertTitle>
<AlertDescription className="mr-2">
<Trans>
Delete your account and all its contents, including completed documents. This action
is irreversible and will cancel your subscription, so proceed with caution.
</Trans>
</AlertDescription>
</div>
<div className="flex-shrink-0">
<Dialog onOpenChange={() => setEnteredEmail('')}>
<DialogTrigger asChild>
<Button variant="destructive">
<Trans>Delete Account</Trans>
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader className="space-y-4">
<DialogTitle>
<Trans>Delete Account</Trans>
</DialogTitle>
<Alert variant="destructive">
<AlertDescription className="selection:bg-red-100">
<Trans>This action is not reversible. Please be certain.</Trans>
</AlertDescription>
</Alert>
{hasTwoFactorAuthentication && (
<Alert variant="destructive">
<AlertDescription className="selection:bg-red-100">
<Trans>Disable Two Factor Authentication before deleting your account.</Trans>
</AlertDescription>
</Alert>
)}
<DialogDescription>
<Trans>
Documenso will delete{' '}
<span className="font-semibold">all of your documents</span>, along with all of
your completed documents, signatures, and all other resources belonging to your
Account.
</Trans>
</DialogDescription>
</DialogHeader>
{!hasTwoFactorAuthentication && (
<div>
<Label>
<Trans>
Please type{' '}
<span className="text-muted-foreground font-semibold">{user.email}</span> to
confirm.
</Trans>
</Label>
<Input
type="text"
className="mt-2"
aria-label="Confirm Email"
value={enteredEmail}
onChange={(e) => setEnteredEmail(e.target.value)}
/>
</div>
)}
<DialogFooter>
<Button
onClick={onDeleteAccount}
loading={isDeletingAccount}
variant="destructive"
disabled={hasTwoFactorAuthentication || enteredEmail !== user.email}
>
{isDeletingAccount ? _(msg`Deleting account...`) : _(msg`Confirm Deletion`)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</Alert>
</div>
);
};

View File

@ -16,24 +16,26 @@ import {
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer'; import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team';
type DocumentDuplicateDialogProps = { type DocumentDuplicateDialogProps = {
id: number; id: number;
open: boolean; open: boolean;
onOpenChange: (_open: boolean) => void; onOpenChange: (_open: boolean) => void;
team?: Pick<Team, 'id' | 'url'>;
}; };
export const DocumentDuplicateDialog = ({ export const DocumentDuplicateDialog = ({
id, id,
open, open,
onOpenChange, onOpenChange,
team,
}: DocumentDuplicateDialogProps) => { }: DocumentDuplicateDialogProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const { toast } = useToast(); const { toast } = useToast();
const { _ } = useLingui(); const { _ } = useLingui();
const team = useOptionalCurrentTeam();
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery({ const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery({
documentId: id, documentId: id,
}); });

View File

@ -36,6 +36,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { StackAvatar } from '~/components/(dashboard)/avatar/stack-avatar'; import { StackAvatar } from '~/components/(dashboard)/avatar/stack-avatar';
import { useAuth } from '~/providers/auth'; import { useAuth } from '~/providers/auth';
import { useOptionalCurrentTeam } from '~/providers/team';
const FORM_ID = 'resend-email'; const FORM_ID = 'resend-email';
@ -44,7 +45,6 @@ export type DocumentResendDialogProps = {
team: Pick<Team, 'id' | 'url'> | null; team: Pick<Team, 'id' | 'url'> | null;
}; };
recipients: Recipient[]; recipients: Recipient[];
team?: Pick<Team, 'id' | 'url'>;
}; };
export const ZResendDocumentFormSchema = z.object({ export const ZResendDocumentFormSchema = z.object({
@ -55,8 +55,9 @@ export const ZResendDocumentFormSchema = z.object({
export type TResendDocumentFormSchema = z.infer<typeof ZResendDocumentFormSchema>; export type TResendDocumentFormSchema = z.infer<typeof ZResendDocumentFormSchema>;
export const DocumentResendDialog = ({ document, recipients, team }: DocumentResendDialogProps) => { export const DocumentResendDialog = ({ document, recipients }: DocumentResendDialogProps) => {
const { user } = useAuth(); const { user } = useAuth();
const team = useOptionalCurrentTeam();
const { toast } = useToast(); const { toast } = useToast();
const { _ } = useLingui(); const { _ } = useLingui();

View File

@ -20,18 +20,18 @@ import {
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs'; import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type CreateTeamCheckoutDialogProps = { export type TeamCheckoutCreateDialogProps = {
pendingTeamId: number | null; pendingTeamId: number | null;
onClose: () => void; onClose: () => void;
} & Omit<DialogPrimitive.DialogProps, 'children'>; } & Omit<DialogPrimitive.DialogProps, 'children'>;
const MotionCard = motion(Card); const MotionCard = motion(Card);
export const CreateTeamCheckoutDialog = ({ export const TeamCheckoutCreateDialog = ({
pendingTeamId, pendingTeamId,
onClose, onClose,
...props ...props
}: CreateTeamCheckoutDialogProps) => { }: TeamCheckoutCreateDialogProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();

View File

@ -35,7 +35,7 @@ import {
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type CreateTeamDialogProps = { export type TeamCreateDialogProps = {
trigger?: React.ReactNode; trigger?: React.ReactNode;
} & Omit<DialogPrimitive.DialogProps, 'children'>; } & Omit<DialogPrimitive.DialogProps, 'children'>;
@ -46,7 +46,7 @@ const ZCreateTeamFormSchema = ZCreateTeamMutationSchema.pick({
type TCreateTeamFormSchema = z.infer<typeof ZCreateTeamFormSchema>; type TCreateTeamFormSchema = z.infer<typeof ZCreateTeamFormSchema>;
export const CreateTeamDialog = ({ trigger, ...props }: CreateTeamDialogProps) => { export const TeamCreateDialog = ({ trigger, ...props }: TeamCreateDialogProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();

View File

@ -30,13 +30,13 @@ import { Input } from '@documenso/ui/primitives/input';
import type { Toast } from '@documenso/ui/primitives/use-toast'; import type { Toast } from '@documenso/ui/primitives/use-toast';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type DeleteTeamDialogProps = { export type TeamDeleteDialogProps = {
teamId: number; teamId: number;
teamName: string; teamName: string;
trigger?: React.ReactNode; trigger?: React.ReactNode;
}; };
export const DeleteTeamDialog = ({ trigger, teamId, teamName }: DeleteTeamDialogProps) => { export const TeamDeleteDialog = ({ trigger, teamId, teamName }: TeamDeleteDialogProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);

View File

@ -32,7 +32,7 @@ import {
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type AddTeamEmailDialogProps = { export type TeamEmailAddDialogProps = {
teamId: number; teamId: number;
trigger?: React.ReactNode; trigger?: React.ReactNode;
} & Omit<DialogPrimitive.DialogProps, 'children'>; } & Omit<DialogPrimitive.DialogProps, 'children'>;
@ -44,7 +44,7 @@ const ZCreateTeamEmailFormSchema = ZCreateTeamEmailVerificationMutationSchema.pi
type TCreateTeamEmailFormSchema = z.infer<typeof ZCreateTeamEmailFormSchema>; type TCreateTeamEmailFormSchema = z.infer<typeof ZCreateTeamEmailFormSchema>;
export const AddTeamEmailDialog = ({ teamId, trigger, ...props }: AddTeamEmailDialogProps) => { export const TeamEmailAddDialog = ({ teamId, trigger, ...props }: TeamEmailAddDialogProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { _ } = useLingui(); const { _ } = useLingui();

View File

@ -21,7 +21,7 @@ import {
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type RemoveTeamEmailDialogProps = { export type TeamEmailDeleteDialogProps = {
trigger?: React.ReactNode; trigger?: React.ReactNode;
teamName: string; teamName: string;
team: Prisma.TeamGetPayload<{ team: Prisma.TeamGetPayload<{
@ -38,7 +38,7 @@ export type RemoveTeamEmailDialogProps = {
}>; }>;
}; };
export const RemoveTeamEmailDialog = ({ trigger, teamName, team }: RemoveTeamEmailDialogProps) => { export const TeamEmailDeleteDialog = ({ trigger, teamName, team }: TeamEmailDeleteDialogProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { _ } = useLingui(); const { _ } = useLingui();

View File

@ -30,7 +30,7 @@ import {
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type UpdateTeamEmailDialogProps = { export type TeamEmailUpdateDialogProps = {
teamEmail: TeamEmail; teamEmail: TeamEmail;
trigger?: React.ReactNode; trigger?: React.ReactNode;
} & Omit<DialogPrimitive.DialogProps, 'children'>; } & Omit<DialogPrimitive.DialogProps, 'children'>;
@ -41,11 +41,11 @@ const ZUpdateTeamEmailFormSchema = z.object({
type TUpdateTeamEmailFormSchema = z.infer<typeof ZUpdateTeamEmailFormSchema>; type TUpdateTeamEmailFormSchema = z.infer<typeof ZUpdateTeamEmailFormSchema>;
export const UpdateTeamEmailDialog = ({ export const TeamEmailUpdateDialog = ({
teamEmail, teamEmail,
trigger, trigger,
...props ...props
}: UpdateTeamEmailDialogProps) => { }: TeamEmailUpdateDialogProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { _ } = useLingui(); const { _ } = useLingui();

View File

@ -21,7 +21,7 @@ import {
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type LeaveTeamDialogProps = { export type TeamLeaveDialogProps = {
teamId: number; teamId: number;
teamName: string; teamName: string;
teamAvatarImageId?: string | null; teamAvatarImageId?: string | null;
@ -29,13 +29,13 @@ export type LeaveTeamDialogProps = {
trigger?: React.ReactNode; trigger?: React.ReactNode;
}; };
export const LeaveTeamDialog = ({ export const TeamLeaveDialog = ({
trigger, trigger,
teamId, teamId,
teamName, teamName,
teamAvatarImageId, teamAvatarImageId,
role, role,
}: LeaveTeamDialogProps) => { }: TeamLeaveDialogProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { _ } = useLingui(); const { _ } = useLingui();

View File

@ -18,7 +18,7 @@ import {
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type DeleteTeamMemberDialogProps = { export type TeamMemberDeleteDialogProps = {
teamId: number; teamId: number;
teamName: string; teamName: string;
teamMemberId: number; teamMemberId: number;
@ -27,14 +27,14 @@ export type DeleteTeamMemberDialogProps = {
trigger?: React.ReactNode; trigger?: React.ReactNode;
}; };
export const DeleteTeamMemberDialog = ({ export const TeamMemberDeleteDialog = ({
trigger, trigger,
teamId, teamId,
teamName, teamName,
teamMemberId, teamMemberId,
teamMemberName, teamMemberName,
teamMemberEmail, teamMemberEmail,
}: DeleteTeamMemberDialogProps) => { }: TeamMemberDeleteDialogProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { _ } = useLingui(); const { _ } = useLingui();

View File

@ -45,7 +45,7 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type InviteTeamMembersDialogProps = { export type TeamMemberInviteDialogProps = {
currentUserTeamRole: TeamMemberRole; currentUserTeamRole: TeamMemberRole;
teamId: number; teamId: number;
trigger?: React.ReactNode; trigger?: React.ReactNode;
@ -94,12 +94,12 @@ const ZImportTeamMemberSchema = z.array(
}), }),
); );
export const InviteTeamMembersDialog = ({ export const TeamMemberInviteDialog = ({
currentUserTeamRole, currentUserTeamRole,
teamId, teamId,
trigger, trigger,
...props ...props
}: InviteTeamMembersDialogProps) => { }: TeamMemberInviteDialogProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const [invitationType, setInvitationType] = useState<TabTypes>('INDIVIDUAL'); const [invitationType, setInvitationType] = useState<TabTypes>('INDIVIDUAL');

View File

@ -38,7 +38,7 @@ import {
} from '@documenso/ui/primitives/select'; } from '@documenso/ui/primitives/select';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type UpdateTeamMemberDialogProps = { export type TeamMemberUpdateDialogProps = {
currentUserTeamRole: TeamMemberRole; currentUserTeamRole: TeamMemberRole;
trigger?: React.ReactNode; trigger?: React.ReactNode;
teamId: number; teamId: number;
@ -53,7 +53,7 @@ const ZUpdateTeamMemberFormSchema = z.object({
type ZUpdateTeamMemberSchema = z.infer<typeof ZUpdateTeamMemberFormSchema>; type ZUpdateTeamMemberSchema = z.infer<typeof ZUpdateTeamMemberFormSchema>;
export const UpdateTeamMemberDialog = ({ export const TeamMemberUpdateDialog = ({
currentUserTeamRole, currentUserTeamRole,
trigger, trigger,
teamId, teamId,
@ -61,7 +61,7 @@ export const UpdateTeamMemberDialog = ({
teamMemberName, teamMemberName,
teamMemberRole, teamMemberRole,
...props ...props
}: UpdateTeamMemberDialogProps) => { }: TeamMemberUpdateDialogProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { _ } = useLingui(); const { _ } = useLingui();

View File

@ -38,19 +38,19 @@ import {
} from '@documenso/ui/primitives/select'; } from '@documenso/ui/primitives/select';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type TransferTeamDialogProps = { export type TeamTransferDialogProps = {
teamId: number; teamId: number;
teamName: string; teamName: string;
ownerUserId: number; ownerUserId: number;
trigger?: React.ReactNode; trigger?: React.ReactNode;
}; };
export const TransferTeamDialog = ({ export const TeamTransferDialog = ({
trigger, trigger,
teamId, teamId,
teamName, teamName,
ownerUserId, ownerUserId,
}: TransferTeamDialogProps) => { }: TeamTransferDialogProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { _ } = useLingui(); const { _ } = useLingui();

View File

@ -8,7 +8,6 @@ import { z } from 'zod';
import { downloadFile } from '@documenso/lib/client-only/download-file'; import { downloadFile } from '@documenso/lib/client-only/download-file';
import { AppError } from '@documenso/lib/errors/app-error'; import { AppError } from '@documenso/lib/errors/app-error';
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -145,7 +144,7 @@ export const ViewRecoveryCodesDialog = () => {
<Alert variant="destructive"> <Alert variant="destructive">
<AlertDescription> <AlertDescription>
{match(AppError.parseError(error).message) {match(AppError.parseError(error).message)
.with(ErrorCode.INCORRECT_TWO_FACTOR_CODE, () => ( .with('INCORRECT_TWO_FACTOR_CODE', () => (
<Trans>Invalid code. Please try again.</Trans> <Trans>Invalid code. Please try again.</Trans>
)) ))
.otherwise(() => ( .otherwise(() => (

View File

@ -3,14 +3,14 @@ import { useMemo } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
// Todo import { ErrorCode, useDropzone } from 'react-dropzone';
// import { ErrorCode, useDropzone } from 'react-dropzone';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { z } from 'zod'; import { z } from 'zod';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError } from '@documenso/lib/errors/app-error'; import { AppError } from '@documenso/lib/errors/app-error';
import { formatAvatarUrl } from '@documenso/lib/utils/avatars'; import { base64 } from '@documenso/lib/universal/base64';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter'; import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
@ -40,9 +40,9 @@ export type AvatarImageFormProps = {
}; };
export const AvatarImageForm = ({ className }: AvatarImageFormProps) => { export const AvatarImageForm = ({ className }: AvatarImageFormProps) => {
const { user } = useAuth();
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { user } = useAuth();
const team = useOptionalCurrentTeam(); const team = useOptionalCurrentTeam();
@ -67,31 +67,31 @@ export const AvatarImageForm = ({ className }: AvatarImageFormProps) => {
resolver: zodResolver(ZAvatarImageFormSchema), resolver: zodResolver(ZAvatarImageFormSchema),
}); });
// const { getRootProps, getInputProps } = useDropzone({ const { getRootProps, getInputProps } = useDropzone({
// maxSize: 1024 * 1024, maxSize: 1024 * 1024,
// accept: { accept: {
// 'image/*': ['.png', '.jpg', '.jpeg'], 'image/*': ['.png', '.jpg', '.jpeg'],
// }, },
// multiple: false, multiple: false,
// onDropAccepted: ([file]) => { onDropAccepted: ([file]) => {
// void file.arrayBuffer().then((buffer) => { void file.arrayBuffer().then((buffer) => {
// const contents = base64.encode(new Uint8Array(buffer)); const contents = base64.encode(new Uint8Array(buffer));
// form.setValue('bytes', contents); form.setValue('bytes', contents);
// void form.handleSubmit(onFormSubmit)(); void form.handleSubmit(onFormSubmit)();
// }); });
// }, },
// onDropRejected: ([file]) => { onDropRejected: ([file]) => {
// form.setError('bytes', { form.setError('bytes', {
// type: 'onChange', type: 'onChange',
// message: match(file.errors[0].code) message: match(file.errors[0].code)
// .with(ErrorCode.FileTooLarge, () => _(msg`Uploaded file is too large`)) .with(ErrorCode.FileTooLarge, () => _(msg`Uploaded file is too large`))
// .with(ErrorCode.FileTooSmall, () => _(msg`Uploaded file is too small`)) .with(ErrorCode.FileTooSmall, () => _(msg`Uploaded file is too small`))
// .with(ErrorCode.FileInvalidType, () => _(msg`Uploaded file not an allowed file type`)) .with(ErrorCode.FileInvalidType, () => _(msg`Uploaded file not an allowed file type`))
// .otherwise(() => _(msg`An unknown error occurred`)), .otherwise(() => _(msg`An unknown error occurred`)),
// }); });
// }, },
// }); });
const onFormSubmit = async (data: TAvatarImageFormSchema) => { const onFormSubmit = async (data: TAvatarImageFormSchema) => {
try { try {
@ -106,7 +106,8 @@ export const AvatarImageForm = ({ className }: AvatarImageFormProps) => {
duration: 5000, duration: 5000,
}); });
// router.refresh(); // Todo // Todo
// router.refresh();
} catch (err) { } catch (err) {
const error = AppError.parseError(err); const error = AppError.parseError(err);
@ -143,7 +144,11 @@ export const AvatarImageForm = ({ className }: AvatarImageFormProps) => {
<div className="flex items-center gap-8"> <div className="flex items-center gap-8">
<div className="relative"> <div className="relative">
<Avatar className="h-16 w-16 border-2 border-solid"> <Avatar className="h-16 w-16 border-2 border-solid">
{avatarImageId && <AvatarImage src={formatAvatarUrl(avatarImageId)} />} {avatarImageId && (
<AvatarImage
src={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${avatarImageId}`}
/>
)}
<AvatarFallback className="text-sm text-gray-400"> <AvatarFallback className="text-sm text-gray-400">
{initials} {initials}
</AvatarFallback> </AvatarFallback>
@ -165,12 +170,12 @@ export const AvatarImageForm = ({ className }: AvatarImageFormProps) => {
type="button" type="button"
variant="secondary" variant="secondary"
size="sm" size="sm"
// {...getRootProps()} {...getRootProps()}
loading={form.formState.isSubmitting} loading={form.formState.isSubmitting}
disabled={form.formState.isSubmitting} disabled={form.formState.isSubmitting}
> >
<Trans>Upload Avatar</Trans> <Trans>Upload Avatar</Trans>
{/* <input {...getInputProps()} /> */} <input {...getInputProps()} />
</Button> </Button>
</div> </div>
</FormControl> </FormControl>

View File

@ -6,8 +6,8 @@ import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { z } from 'zod'; import { z } from 'zod';
import { authClient } from '@documenso/auth/client';
import { AppError } from '@documenso/lib/errors/app-error'; import { AppError } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { ZCurrentPasswordSchema, ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema'; import { ZCurrentPasswordSchema, ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -55,11 +55,9 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
const isSubmitting = form.formState.isSubmitting; const isSubmitting = form.formState.isSubmitting;
const { mutateAsync: updatePassword } = trpc.profile.updatePassword.useMutation();
const onFormSubmit = async ({ currentPassword, password }: TPasswordFormSchema) => { const onFormSubmit = async ({ currentPassword, password }: TPasswordFormSchema) => {
try { try {
await updatePassword({ await authClient.updatePassword({
currentPassword, currentPassword,
password, password,
}); });

View File

@ -88,12 +88,12 @@ export const ClaimPublicProfileDialogForm = ({
} catch (err) { } catch (err) {
const error = AppError.parseError(err); const error = AppError.parseError(err);
if (error.code === AppErrorCode.PROFILE_URL_TAKEN) { if (error.code === 'PROFILE_URL_TAKEN') {
form.setError('url', { form.setError('url', {
type: 'manual', type: 'manual',
message: _(msg`This username is already taken`), message: _(msg`This username is already taken`),
}); });
} else if (error.code === AppErrorCode.PREMIUM_PROFILE_URL) { } else if (error.code === 'PREMIUM_PROFILE_URL') {
form.setError('url', { form.setError('url', {
type: 'manual', type: 'manual',
message: error.message, message: error.message,

View File

@ -11,7 +11,7 @@ import { useForm } from 'react-hook-form';
import type { z } from 'zod'; import type { z } from 'zod';
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard'; import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError } from '@documenso/lib/errors/app-error';
import { formatUserProfilePath } from '@documenso/lib/utils/public-profiles'; import { formatUserProfilePath } from '@documenso/lib/utils/public-profiles';
import { import {
MAX_PROFILE_BIO_LENGTH, MAX_PROFILE_BIO_LENGTH,
@ -88,8 +88,8 @@ export const PublicProfileForm = ({
const error = AppError.parseError(err); const error = AppError.parseError(err);
switch (error.code) { switch (error.code) {
case AppErrorCode.PREMIUM_PROFILE_URL: case 'PREMIUM_PROFILE_URL':
case AppErrorCode.PROFILE_URL_TAKEN: case 'PROFILE_URL_TAKEN':
form.setError('url', { form.setError('url', {
type: 'manual', type: 'manual',
message: error.message, message: error.message,

View File

@ -15,7 +15,6 @@ import { z } from 'zod';
import { authClient } from '@documenso/auth/client'; import { authClient } from '@documenso/auth/client';
import { AuthenticationErrorCode } from '@documenso/auth/server/lib/errors/error-codes'; import { AuthenticationErrorCode } from '@documenso/auth/server/lib/errors/error-codes';
import { AppError } from '@documenso/lib/errors/app-error'; import { AppError } from '@documenso/lib/errors/app-error';
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { ZCurrentPasswordSchema } from '@documenso/trpc/server/auth-router/schema'; import { ZCurrentPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
@ -46,8 +45,6 @@ const CommonErrorMessages = {
[AuthenticationErrorCode.AccountDisabled]: msg`This account has been disabled. Please contact support.`, [AuthenticationErrorCode.AccountDisabled]: msg`This account has been disabled. Please contact support.`,
}; };
const TwoFactorEnabledErrorCode = ErrorCode.TWO_FACTOR_MISSING_CREDENTIALS;
const LOGIN_REDIRECT_PATH = '/documents'; const LOGIN_REDIRECT_PATH = '/documents';
export const ZSignInFormSchema = z.object({ export const ZSignInFormSchema = z.object({
@ -90,7 +87,7 @@ export const SignInForm = ({
const [isPasskeyLoading, setIsPasskeyLoading] = useState(false); const [isPasskeyLoading, setIsPasskeyLoading] = useState(false);
const callbackUrl = useMemo(() => { const redirectUrl = useMemo(() => {
// Handle SSR // Handle SSR
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return LOGIN_REDIRECT_PATH; return LOGIN_REDIRECT_PATH;
@ -161,15 +158,16 @@ export const SignInForm = ({
const credential = await startAuthentication(options); const credential = await startAuthentication(options);
const result = await authClient.passkey.signIn({ await authClient.passkey.signIn({
credential: JSON.stringify(credential), credential: JSON.stringify(credential),
csrfToken: sessionId, csrfToken: sessionId,
redirectUrl,
// callbackUrl, // callbackUrl,
// redirect: false, // redirect: false,
}); });
// Todo: Can't use navigate because of embed? // Todo: Can't use navigate because of embed?
window.location.href = callbackUrl; // window.location.href = callbackUrl;
} catch (err) { } catch (err) {
setIsPasskeyLoading(false); setIsPasskeyLoading(false);
@ -208,17 +206,18 @@ export const SignInForm = ({
password, password,
totpCode, totpCode,
backupCode, backupCode,
redirectUrl,
// callbackUrl, // callbackUrl,
// redirect: false, // redirect: false,
}); });
window.location.href = callbackUrl; // window.location.href = callbackUrl; // Todo: Handle redirect.
} catch (err) { } catch (err) {
console.log(err); console.log(err);
const error = AppError.parseError(err); const error = AppError.parseError(err);
if (error.code === TwoFactorEnabledErrorCode) { if (error.code === 'TWO_FACTOR_MISSING_CREDENTIALS') {
setIsTwoFactorAuthenticationDialogOpen(true); setIsTwoFactorAuthenticationDialogOpen(true);
return; return;
} }
@ -257,12 +256,7 @@ export const SignInForm = ({
const onSignInWithGoogleClick = async () => { const onSignInWithGoogleClick = async () => {
try { try {
// await signIn('google', { await authClient.google.signIn(); // Todo: Handle redirect.
// callbackUrl,
// });
const result = await authClient.google.signIn();
console.log(result);
} catch (err) { } catch (err) {
toast({ toast({
title: _(msg`An unknown error occurred`), title: _(msg`An unknown error occurred`),

View File

@ -12,9 +12,9 @@ import { Link, useNavigate, useSearchParams } from 'react-router';
import { z } from 'zod'; import { z } from 'zod';
import communityCardsImage from '@documenso/assets/images/community-cards.png'; import communityCardsImage from '@documenso/assets/images/community-cards.png';
import { authClient } from '@documenso/auth/client';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema'; import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -71,8 +71,8 @@ export const signupErrorMessages: Record<string, MessageDescriptor> = {
SIGNUP_DISABLED: msg`Signups are disabled.`, SIGNUP_DISABLED: msg`Signups are disabled.`,
[AppErrorCode.ALREADY_EXISTS]: msg`User with this email already exists. Please use a different email address.`, [AppErrorCode.ALREADY_EXISTS]: msg`User with this email already exists. Please use a different email address.`,
[AppErrorCode.INVALID_REQUEST]: msg`We were unable to create your account. Please review the information you provided and try again.`, [AppErrorCode.INVALID_REQUEST]: msg`We were unable to create your account. Please review the information you provided and try again.`,
[AppErrorCode.PROFILE_URL_TAKEN]: msg`This username has already been taken`, PROFILE_URL_TAKEN: msg`This username has already been taken`,
[AppErrorCode.PREMIUM_PROFILE_URL]: msg`Only subscribers can have a username shorter than 6 characters`, PREMIUM_PROFILE_URL: msg`Only subscribers can have a username shorter than 6 characters`,
}; };
export type TSignUpFormSchema = z.infer<typeof ZSignUpFormSchema>; export type TSignUpFormSchema = z.infer<typeof ZSignUpFormSchema>;
@ -93,7 +93,7 @@ export const SignUpForm = ({
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
// const analytics = useAnalytics(); // const analytics = useAnalytics(); // Todo
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@ -120,11 +120,9 @@ export const SignUpForm = ({
const name = form.watch('name'); const name = form.watch('name');
const url = form.watch('url'); const url = form.watch('url');
const { mutateAsync: signup } = trpc.auth.signup.useMutation();
const onFormSubmit = async ({ name, email, password, signature, url }: TSignUpFormSchema) => { const onFormSubmit = async ({ name, email, password, signature, url }: TSignUpFormSchema) => {
try { try {
await signup({ name, email, password, signature, url }); await authClient.emailPassword.signUp({ name, email, password, signature, url });
void navigate(`/unverified-account`); void navigate(`/unverified-account`);
@ -146,10 +144,7 @@ export const SignUpForm = ({
const errorMessage = signupErrorMessages[error.code] ?? signupErrorMessages.INVALID_REQUEST; const errorMessage = signupErrorMessages[error.code] ?? signupErrorMessages.INVALID_REQUEST;
if ( if (error.code === 'PROFILE_URL_TAKEN' || error.code === 'PREMIUM_PROFILE_URL') {
error.code === AppErrorCode.PROFILE_URL_TAKEN ||
error.code === AppErrorCode.PREMIUM_PROFILE_URL
) {
form.setError('url', { form.setError('url', {
type: 'manual', type: 'manual',
message: _(errorMessage), message: _(errorMessage),
@ -175,8 +170,7 @@ export const SignUpForm = ({
const onSignUpWithGoogleClick = async () => { const onSignUpWithGoogleClick = async () => {
try { try {
await new Promise((resolve) => setTimeout(resolve, 2000)); await authClient.google.signIn();
// await signIn('google', { callbackUrl: SIGN_UP_REDIRECT_PATH });
} catch (err) { } catch (err) {
toast({ toast({
title: _(msg`An unknown error occurred`), title: _(msg`An unknown error occurred`),

View File

@ -1,169 +0,0 @@
'use client';
import { useEffect, useMemo, useState, useTransition } from 'react';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { ChevronDownIcon as CaretSortIcon, Loader } from 'lucide-react';
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { Input } from '@documenso/ui/primitives/input';
export type SigningVolume = {
id: number;
name: string;
signingVolume: number;
createdAt: Date;
planId: string;
};
type LeaderboardTableProps = {
signingVolume: SigningVolume[];
totalPages: number;
perPage: number;
page: number;
sortBy: 'name' | 'createdAt' | 'signingVolume';
sortOrder: 'asc' | 'desc';
};
export const LeaderboardTable = ({
signingVolume,
totalPages,
perPage,
page,
sortBy,
sortOrder,
}: LeaderboardTableProps) => {
const { _, i18n } = useLingui();
const [isPending, startTransition] = useTransition();
const updateSearchParams = useUpdateSearchParams();
const [searchString, setSearchString] = useState('');
const debouncedSearchString = useDebouncedValue(searchString, 1000);
const columns = useMemo(() => {
return [
{
header: () => (
<div
className="flex cursor-pointer items-center"
onClick={() => handleColumnSort('name')}
>
{_(msg`Name`)}
<CaretSortIcon className="ml-2 h-4 w-4" />
</div>
),
accessorKey: 'name',
cell: ({ row }) => {
return (
<div>
<a
className="text-primary underline"
href={`https://dashboard.stripe.com/subscriptions/${row.original.planId}`}
target="_blank"
>
{row.getValue('name')}
</a>
</div>
);
},
size: 250,
},
{
header: () => (
<div
className="flex cursor-pointer items-center"
onClick={() => handleColumnSort('signingVolume')}
>
{_(msg`Signing Volume`)}
<CaretSortIcon className="ml-2 h-4 w-4" />
</div>
),
accessorKey: 'signingVolume',
cell: ({ row }) => <div>{Number(row.getValue('signingVolume'))}</div>,
},
{
header: () => {
return (
<div
className="flex cursor-pointer items-center"
onClick={() => handleColumnSort('createdAt')}
>
{_(msg`Created`)}
<CaretSortIcon className="ml-2 h-4 w-4" />
</div>
);
},
accessorKey: 'createdAt',
cell: ({ row }) => i18n.date(row.original.createdAt),
},
] satisfies DataTableColumnDef<SigningVolume>[];
}, [sortOrder]);
useEffect(() => {
startTransition(() => {
updateSearchParams({
search: debouncedSearchString,
page: 1,
perPage,
sortBy,
sortOrder,
});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedSearchString]);
const onPaginationChange = (page: number, perPage: number) => {
startTransition(() => {
updateSearchParams({
page,
perPage,
});
});
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchString(e.target.value);
};
const handleColumnSort = (column: 'name' | 'createdAt' | 'signingVolume') => {
startTransition(() => {
updateSearchParams({
sortBy: column,
sortOrder: sortBy === column && sortOrder === 'asc' ? 'desc' : 'asc',
});
});
};
return (
<div className="relative">
<Input
className="my-6 flex flex-row gap-4"
type="text"
placeholder={_(msg`Search by name or email`)}
value={searchString}
onChange={handleChange}
/>
<DataTable
columns={columns}
data={signingVolume}
perPage={perPage}
currentPage={page}
totalPages={totalPages}
onPaginationChange={onPaginationChange}
>
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
</DataTable>
{isPending && (
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
<Loader className="h-8 w-8 animate-spin text-gray-500" />
</div>
)}
</div>
);
};

View File

@ -1,25 +0,0 @@
'use server';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
import { getSigningVolume } from '@documenso/lib/server-only/admin/get-signing-volume';
type SearchOptions = {
search: string;
page: number;
perPage: number;
sortBy: 'name' | 'createdAt' | 'signingVolume';
sortOrder: 'asc' | 'desc';
};
export async function search({ search, page, perPage, sortBy, sortOrder }: SearchOptions) {
const { user } = await getRequiredServerComponentSession();
if (!isAdmin(user)) {
throw new Error('Unauthorized');
}
const results = await getSigningVolume({ search, page, perPage, sortBy, sortOrder });
return results;
}

View File

@ -1,48 +0,0 @@
import { Trans } from '@lingui/macro';
import { LeaderboardTable } from './data-table-leaderboard';
import { search } from './fetch-leaderboard.actions';
type AdminLeaderboardProps = {
searchParams?: {
search?: string;
page?: number;
perPage?: number;
sortBy?: 'name' | 'createdAt' | 'signingVolume';
sortOrder?: 'asc' | 'desc';
};
};
export default async function LeaderboardPage({ searchParams = {} }: AdminLeaderboardProps) {
const page = Number(searchParams.page) || 1;
const perPage = Number(searchParams.perPage) || 10;
const searchString = searchParams.search || '';
const sortBy = searchParams.sortBy || 'signingVolume';
const sortOrder = searchParams.sortOrder || 'desc';
const { leaderboard: signingVolume, totalPages } = await search({
search: searchString,
page,
perPage,
sortBy,
sortOrder,
});
return (
<div>
<h2 className="text-4xl font-semibold">
<Trans>Signing Volume</Trans>
</h2>
<div className="mt-8">
<LeaderboardTable
signingVolume={signingVolume}
totalPages={totalPages}
page={page}
perPage={perPage}
sortBy={sortBy}
sortOrder={sortOrder}
/>
</div>
</div>
);
}

View File

@ -9,13 +9,15 @@ import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type DownloadAuditLogButtonProps = { export type DocumentAuditLogDownloadButtonProps = {
className?: string; className?: string;
teamId?: number;
documentId: number; documentId: number;
}; };
export const DownloadAuditLogButton = ({ className, documentId }: DownloadAuditLogButtonProps) => { export const DocumentAuditLogDownloadButton = ({
className,
documentId,
}: DocumentAuditLogDownloadButtonProps) => {
const { toast } = useToast(); const { toast } = useToast();
const { _ } = useLingui(); const { _ } = useLingui();

View File

@ -1,5 +1,3 @@
'use client';
import { Trans, msg } from '@lingui/macro'; import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { DocumentStatus } from '@prisma/client'; import { DocumentStatus } from '@prisma/client';
@ -10,19 +8,17 @@ import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type DownloadCertificateButtonProps = { export type DocumentCertificateDownloadButtonProps = {
className?: string; className?: string;
documentId: number; documentId: number;
documentStatus: DocumentStatus; documentStatus: DocumentStatus;
teamId?: number;
}; };
export const DownloadCertificateButton = ({ export const DocumentCertificateDownloadButton = ({
className, className,
documentId, documentId,
documentStatus, documentStatus,
teamId, }: DocumentCertificateDownloadButtonProps) => {
}: DownloadCertificateButtonProps) => {
const { toast } = useToast(); const { toast } = useToast();
const { _ } = useLingui(); const { _ } = useLingui();

View File

@ -3,6 +3,7 @@ import { useEffect, useState } from 'react';
import { msg } from '@lingui/macro'; import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { DocumentDistributionMethod, DocumentStatus } from '@prisma/client'; import { DocumentDistributionMethod, DocumentStatus } from '@prisma/client';
import { useNavigate, useSearchParams } from 'react-router';
import { isValidLanguageCode } from '@documenso/lib/constants/i18n'; import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
import { import {
@ -29,7 +30,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team'; import { useOptionalCurrentTeam } from '~/providers/team';
export type EditDocumentFormProps = { export type DocumentEditFormProps = {
className?: string; className?: string;
initialDocument: TDocument; initialDocument: TDocument;
documentRootPath: string; documentRootPath: string;
@ -39,17 +40,18 @@ export type EditDocumentFormProps = {
type EditDocumentStep = 'settings' | 'signers' | 'fields' | 'subject'; type EditDocumentStep = 'settings' | 'signers' | 'fields' | 'subject';
const EditDocumentSteps: EditDocumentStep[] = ['settings', 'signers', 'fields', 'subject']; const EditDocumentSteps: EditDocumentStep[] = ['settings', 'signers', 'fields', 'subject'];
export const EditDocumentForm = ({ export const DocumentEditForm = ({
className, className,
initialDocument, initialDocument,
documentRootPath, documentRootPath,
isDocumentEnterprise, isDocumentEnterprise,
}: EditDocumentFormProps) => { }: DocumentEditFormProps) => {
const { toast } = useToast(); const { toast } = useToast();
const { _ } = useLingui(); const { _ } = useLingui();
const router = useRouter(); const navigate = useNavigate();
const searchParams = useSearchParams();
const [searchParams] = useSearchParams();
const team = useOptionalCurrentTeam(); const team = useOptionalCurrentTeam();
const [isDocumentPdfLoaded, setIsDocumentPdfLoaded] = useState(false); const [isDocumentPdfLoaded, setIsDocumentPdfLoaded] = useState(false);
@ -194,9 +196,6 @@ export const EditDocumentForm = ({
}, },
}); });
// Router refresh is here to clear the router cache for when navigating to /documents.
router.refresh();
setStep('signers'); setStep('signers');
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -227,9 +226,6 @@ export const EditDocumentForm = ({
}), }),
]); ]);
// Router refresh is here to clear the router cache for when navigating to /documents.
router.refresh();
setStep('fields'); setStep('fields');
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -265,9 +261,6 @@ export const EditDocumentForm = ({
} }
} }
// Router refresh is here to clear the router cache for when navigating to /documents.
router.refresh();
setStep('subject'); setStep('subject');
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -301,7 +294,7 @@ export const EditDocumentForm = ({
duration: 5000, duration: 5000,
}); });
router.push(documentRootPath); void navigate(documentRootPath);
return; return;
} }
@ -312,7 +305,7 @@ export const EditDocumentForm = ({
duration: 5000, duration: 5000,
}); });
} else { } else {
router.push(`${documentRootPath}/${document.id}`); void navigate(`${documentRootPath}/${document.id}`);
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);

View File

@ -3,7 +3,7 @@ import { useLingui } from '@lingui/react';
import type { Document, Recipient, Team, User } from '@prisma/client'; import type { Document, Recipient, Team, User } from '@prisma/client';
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client'; import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
import { CheckCircle, Download, EyeIcon, Pencil } from 'lucide-react'; import { CheckCircle, Download, EyeIcon, Pencil } from 'lucide-react';
import { useSession } from 'next-auth/react'; import { Link } from 'react-router';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf'; import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
@ -12,25 +12,23 @@ import { trpc as trpcClient } from '@documenso/trpc/client';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { useAuth } from '~/providers/auth';
export type DocumentPageViewButtonProps = { export type DocumentPageViewButtonProps = {
document: Document & { document: Document & {
user: Pick<User, 'id' | 'name' | 'email'>; user: Pick<User, 'id' | 'name' | 'email'>;
recipients: Recipient[]; recipients: Recipient[];
team: Pick<Team, 'id' | 'url'> | null; team: Pick<Team, 'id' | 'url'> | null;
}; };
team?: Pick<Team, 'id' | 'url'>;
}; };
export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps) => { export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps) => {
const { data: session } = useSession(); const { user } = useAuth();
const { toast } = useToast(); const { toast } = useToast();
const { _ } = useLingui(); const { _ } = useLingui();
if (!session) { const recipient = document.recipients.find((recipient) => recipient.email === user.email);
return null;
}
const recipient = document.recipients.find((recipient) => recipient.email === session.user.email);
const isRecipient = !!recipient; const isRecipient = !!recipient;
const isPending = document.status === DocumentStatus.PENDING; const isPending = document.status === DocumentStatus.PENDING;
@ -77,7 +75,7 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
}) })
.with({ isRecipient: true, isPending: true, isSigned: false }, () => ( .with({ isRecipient: true, isPending: true, isSigned: false }, () => (
<Button className="w-full" asChild> <Button className="w-full" asChild>
<Link href={`/sign/${recipient?.token}`}> <Link to={`/sign/${recipient?.token}`}>
{match(role) {match(role)
.with(RecipientRole.SIGNER, () => ( .with(RecipientRole.SIGNER, () => (
<> <>
@ -102,7 +100,7 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
)) ))
.with({ isComplete: false }, () => ( .with({ isComplete: false }, () => (
<Button className="w-full" asChild> <Button className="w-full" asChild>
<Link href={`${documentsPath}/${document.id}/edit`}> <Link to={`${documentsPath}/${document.id}/edit`}>
<Trans>Edit</Trans> <Trans>Edit</Trans>
</Link> </Link>
</Button> </Button>

View File

@ -3,7 +3,7 @@ import { useState } from 'react';
import { Trans, msg } from '@lingui/macro'; import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { DocumentStatus } from '@prisma/client'; import { DocumentStatus } from '@prisma/client';
import type { Document, Recipient, Team, TeamEmail, User } from '@prisma/client'; import type { Document, Recipient, Team, User } from '@prisma/client';
import { import {
Copy, Copy,
Download, Download,
@ -14,7 +14,7 @@ import {
Share, Share,
Trash2, Trash2,
} from 'lucide-react'; } from 'lucide-react';
import { useSession } from 'next-auth/react'; import { Link } from 'react-router';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf'; import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
@ -29,11 +29,12 @@ import {
} from '@documenso/ui/primitives/dropdown-menu'; } from '@documenso/ui/primitives/dropdown-menu';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { DocumentDeleteDialog } from '~/components/dialogs/document-delete-dialog';
import { DocumentDuplicateDialog } from '~/components/dialogs/document-duplicate-dialog';
import { DocumentResendDialog } from '~/components/dialogs/document-resend-dialog';
import { DocumentRecipientLinkCopyDialog } from '~/components/document/document-recipient-link-copy-dialog'; import { DocumentRecipientLinkCopyDialog } from '~/components/document/document-recipient-link-copy-dialog';
import { useAuth } from '~/providers/auth';
import { DeleteDocumentDialog } from '../delete-document-dialog'; import { useOptionalCurrentTeam } from '~/providers/team';
import { DuplicateDocumentDialog } from '../duplicate-document-dialog';
import { ResendDocumentActionItem } from '../resend-document-dialog';
export type DocumentPageViewDropdownProps = { export type DocumentPageViewDropdownProps = {
document: Document & { document: Document & {
@ -41,24 +42,21 @@ export type DocumentPageViewDropdownProps = {
recipients: Recipient[]; recipients: Recipient[];
team: Pick<Team, 'id' | 'url'> | null; team: Pick<Team, 'id' | 'url'> | null;
}; };
team?: Pick<Team, 'id' | 'url'> & { teamEmail: TeamEmail | null };
}; };
export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDropdownProps) => { export const DocumentPageViewDropdown = ({ document }: DocumentPageViewDropdownProps) => {
const { data: session } = useSession(); const { user } = useAuth();
const { toast } = useToast(); const { toast } = useToast();
const { _ } = useLingui(); const { _ } = useLingui();
const team = useOptionalCurrentTeam();
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false); const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [isDuplicateDialogOpen, setDuplicateDialogOpen] = useState(false); const [isDuplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
if (!session) { const recipient = document.recipients.find((recipient) => recipient.email === user.email);
return null;
}
const recipient = document.recipients.find((recipient) => recipient.email === session.user.email); const isOwner = document.user.id === user.id;
const isOwner = document.user.id === session.user.id;
const isDraft = document.status === DocumentStatus.DRAFT; const isDraft = document.status === DocumentStatus.DRAFT;
const isPending = document.status === DocumentStatus.PENDING; const isPending = document.status === DocumentStatus.PENDING;
const isDeleted = document.deletedAt !== null; const isDeleted = document.deletedAt !== null;
@ -112,7 +110,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
{(isOwner || isCurrentTeamDocument) && !isComplete && ( {(isOwner || isCurrentTeamDocument) && !isComplete && (
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link href={`${documentsPath}/${document.id}/edit`}> <Link to={`${documentsPath}/${document.id}/edit`}>
<Edit className="mr-2 h-4 w-4" /> <Edit className="mr-2 h-4 w-4" />
<Trans>Edit</Trans> <Trans>Edit</Trans>
</Link> </Link>
@ -127,7 +125,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
)} )}
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link href={`${documentsPath}/${document.id}/logs`}> <Link to={`${documentsPath}/${document.id}/logs`}>
<ScrollTextIcon className="mr-2 h-4 w-4" /> <ScrollTextIcon className="mr-2 h-4 w-4" />
<Trans>Audit Log</Trans> <Trans>Audit Log</Trans>
</Link> </Link>
@ -165,11 +163,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
/> />
)} )}
<ResendDocumentActionItem <DocumentResendDialog document={document} recipients={nonSignedRecipients} />
document={document}
recipients={nonSignedRecipients}
team={team}
/>
<DocumentShareButton <DocumentShareButton
documentId={document.id} documentId={document.id}
@ -185,7 +179,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
/> />
</DropdownMenuContent> </DropdownMenuContent>
<DeleteDocumentDialog <DocumentDeleteDialog
id={document.id} id={document.id}
status={document.status} status={document.status}
documentTitle={document.title} documentTitle={document.title}
@ -195,11 +189,10 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
/> />
{isDuplicateDialogOpen && ( {isDuplicateDialogOpen && (
<DuplicateDocumentDialog <DocumentDuplicateDialog
id={document.id} id={document.id}
open={isDuplicateDialogOpen} open={isDuplicateDialogOpen}
onOpenChange={setDuplicateDialogOpen} onOpenChange={setDuplicateDialogOpen}
team={team}
/> />
)} )}
</DropdownMenu> </DropdownMenu>

View File

@ -1,3 +1,7 @@
'use client';
import Link from 'next/link';
import { Trans, msg } from '@lingui/macro'; import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client'; import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';

View File

@ -1,5 +1,9 @@
'use client';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useSearchParams } from 'next/navigation';
import { msg } from '@lingui/macro'; import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
@ -16,7 +20,7 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { Skeleton } from '@documenso/ui/primitives/skeleton'; import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table'; import { TableCell } from '@documenso/ui/primitives/table';
export type DocumentLogsDataTableProps = { export type DocumentLogsTableProps = {
documentId: number; documentId: number;
}; };
@ -25,7 +29,7 @@ const dateFormat: DateTimeFormatOptions = {
hourCycle: 'h12', hourCycle: 'h12',
}; };
export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps) => { export const DocumentLogsTable = ({ documentId }: DocumentLogsTableProps) => {
const { _, i18n } = useLingui(); const { _, i18n } = useLingui();
const searchParams = useSearchParams(); const searchParams = useSearchParams();

View File

@ -1,159 +0,0 @@
import { Trans } from '@lingui/macro';
import type { Team, TeamEmail, TeamMemberRole } from '@prisma/client';
import { Link } from 'react-router';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import type { PeriodSelectorValue } from '@documenso/lib/server-only/document/find-documents';
import { parseToIntegerArray } from '@documenso/lib/utils/params';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import { trpc } from '@documenso/trpc/react';
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { DocumentSearch } from '~/components/(dashboard)/document-search/document-search';
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
import { isPeriodSelectorValue } from '~/components/(dashboard)/period-selector/types';
import { DocumentUploadDropzone } from '~/components/document/document-upload';
import { DocumentStatus } from '~/components/formatter/document-status';
import { DocumentsTable } from '~/components/tables/documents-table';
import { DocumentsTableEmptyState } from '~/components/tables/documents-table-empty-state';
import { DocumentsTableSenderFilter } from '~/components/tables/documents-table-sender-filter';
import { useAuth } from '~/providers/auth';
export interface DocumentsPageViewProps {
searchParams?: {
status?: ExtendedDocumentStatus;
period?: PeriodSelectorValue;
page?: string;
perPage?: string;
senderIds?: string;
search?: string;
};
team?: Team & { teamEmail?: TeamEmail | null } & { currentTeamMember?: { role: TeamMemberRole } };
}
export const DocumentsPageView = ({ searchParams = {}, team }: DocumentsPageViewProps) => {
const { user } = useAuth();
const status = isExtendedDocumentStatus(searchParams.status) ? searchParams.status : 'ALL';
const period = isPeriodSelectorValue(searchParams.period) ? searchParams.period : '';
const page = Number(searchParams.page) || 1;
const perPage = Number(searchParams.perPage) || 20;
const senderIds = parseToIntegerArray(searchParams.senderIds ?? '');
const search = searchParams.search || '';
const currentTeam = team
? { id: team.id, url: team.url, teamEmail: team.teamEmail?.email }
: undefined;
const currentTeamMemberRole = team?.currentTeamMember?.role;
// const results = await findDocuments({
// status,
// orderBy: {
// column: 'createdAt',
// direction: 'desc',
// },
// page,
// perPage,
// period,
// senderIds,
// query: search,
// });
const { data, isLoading, isLoadingError } = trpc.document.findDocuments.useQuery({
page,
perPage,
});
const getTabHref = (value: typeof status) => {
const params = new URLSearchParams(searchParams);
params.set('status', value);
if (params.has('page')) {
params.delete('page');
}
return `${formatDocumentsPath(team?.url)}?${params.toString()}`;
};
return (
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
<DocumentUploadDropzone team={currentTeam} />
<div className="mt-12 flex flex-wrap items-center justify-between gap-x-4 gap-y-8">
<div className="flex flex-row items-center">
{team && (
<Avatar className="dark:border-border mr-3 h-12 w-12 border-2 border-solid border-white">
{team.avatarImageId && (
<AvatarImage src={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${team.avatarImageId}`} />
)}
<AvatarFallback className="text-xs text-gray-400">
{team.name.slice(0, 1)}
</AvatarFallback>
</Avatar>
)}
<h1 className="text-4xl font-semibold">
<Trans>Documents</Trans>
</h1>
</div>
<div className="-m-1 flex flex-wrap gap-x-4 gap-y-6 overflow-hidden p-1">
<Tabs value={status} className="overflow-x-auto">
<TabsList>
{[
ExtendedDocumentStatus.INBOX,
ExtendedDocumentStatus.PENDING,
ExtendedDocumentStatus.COMPLETED,
ExtendedDocumentStatus.DRAFT,
ExtendedDocumentStatus.ALL,
].map((value) => (
<TabsTrigger
key={value}
className="hover:text-foreground min-w-[60px]"
value={value}
asChild
>
<Link to={getTabHref(value)} preventScrollReset>
<DocumentStatus status={value} />
{value !== ExtendedDocumentStatus.ALL && (
<span className="ml-1 inline-block opacity-50">todo</span>
)}
</Link>
</TabsTrigger>
))}
</TabsList>
</Tabs>
{team && <DocumentsTableSenderFilter teamId={team.id} />}
<div className="flex w-48 flex-wrap items-center justify-between gap-x-2 gap-y-4">
<PeriodSelector />
</div>
<div className="flex w-48 flex-wrap items-center justify-between gap-x-2 gap-y-4">
<DocumentSearch initialValue={search} />
</div>
</div>
</div>
<div className="mt-8">
<div>
{data && data.count === 0 ? (
<DocumentsTableEmptyState status={status} />
) : (
<DocumentsTable
data={data}
isLoading={isLoading}
isLoadingError={isLoadingError}
showSenderColumn={team !== undefined}
team={currentTeam}
/>
)}
</div>
</div>
</div>
);
};

View File

@ -1,16 +0,0 @@
import { useSearchParams } from 'react-router';
export function meta() {
return [{ title: 'Documents' }];
}
export default function DocumentsPage() {
const [searchParams] = useSearchParams();
return (
<>
<div>hello</div>
{/* <DocumentsPageView searchParams={searchParams} /> */}
</>
);
}

View File

@ -5,6 +5,7 @@ import {
Scripts, Scripts,
ScrollRestoration, ScrollRestoration,
isRouteErrorResponse, isRouteErrorResponse,
useLoaderData,
} from 'react-router'; } from 'react-router';
import { TrpcProvider } from '@documenso/trpc/react'; import { TrpcProvider } from '@documenso/trpc/react';
@ -32,7 +33,17 @@ export const links: Route.LinksFunction = () => [
{ rel: 'stylesheet', href: stylesheet }, { rel: 'stylesheet', href: stylesheet },
]; ];
export function loader() {
return {
__ENV__: Object.fromEntries(
Object.entries(process.env).filter(([key]) => key.startsWith('NEXT_')),
),
};
}
export function Layout({ children }: { children: React.ReactNode }) { export function Layout({ children }: { children: React.ReactNode }) {
const { __ENV__ } = useLoaderData<typeof loader>() || {};
return ( return (
<html lang="en"> <html lang="en">
<head> <head>
@ -45,6 +56,12 @@ export function Layout({ children }: { children: React.ReactNode }) {
{children} {children}
<ScrollRestoration /> <ScrollRestoration />
<Scripts /> <Scripts />
<script
dangerouslySetInnerHTML={{
__html: `window.__ENV__ = ${JSON.stringify(__ENV__)}`,
}}
/>
</body> </body>
</html> </html>
); );

View File

@ -22,6 +22,7 @@ import {
getUsersWithSubscriptionsCount, getUsersWithSubscriptionsCount,
} from '@documenso/lib/server-only/admin/get-users-stats'; } from '@documenso/lib/server-only/admin/get-users-stats';
import { getSignerConversionMonthly } from '@documenso/lib/server-only/user/get-signer-conversion'; import { getSignerConversionMonthly } from '@documenso/lib/server-only/user/get-signer-conversion';
import { env } from '@documenso/lib/utils/env';
import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card'; import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card';
import { AdminStatsSignerConversionChart } from '~/components/general/admin-stats-signer-conversion-chart'; import { AdminStatsSignerConversionChart } from '~/components/general/admin-stats-signer-conversion-chart';
@ -87,11 +88,7 @@ export default function AdminStatsPage({ loaderData }: Route.ComponentProps) {
value={usersWithSubscriptionsCount} value={usersWithSubscriptionsCount}
/> />
<CardMetric <CardMetric icon={FileCog} title={_(msg`App Version`)} value={`v${env('APP_VERSION')}`} />
icon={FileCog}
title={_(msg`App Version`)}
value={`v${process.env.APP_VERSION}`}
/>
</div> </div>
<div className="mt-16 gap-8"> <div className="mt-16 gap-8">

View File

@ -1,20 +1,20 @@
import { Plural, Trans } from '@lingui/macro'; import { Plural, Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { DocumentStatus } from '@prisma/client'; import { DocumentStatus } from '@prisma/client';
import type { Team, TeamEmail } from '@prisma/client';
import { TeamMemberRole } from '@prisma/client'; import { TeamMemberRole } from '@prisma/client';
import { ChevronLeft, Clock9, Users2 } from 'lucide-react'; import { ChevronLeft, Clock9, Users2 } from 'lucide-react';
import { Link, redirect } from 'react-router';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { getRequiredSession } from '@documenso/auth/server/lib/utils/get-session';
import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto'; import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document'; import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document'; import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility'; import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { symmetricDecrypt } from '@documenso/lib/universal/crypto'; import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { Badge } from '@documenso/ui/primitives/badge'; import { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card'; import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -28,24 +28,38 @@ import {
DocumentStatus as DocumentStatusComponent, DocumentStatus as DocumentStatusComponent,
FRIENDLY_STATUS_MAP, FRIENDLY_STATUS_MAP,
} from '~/components/formatter/document-status'; } from '~/components/formatter/document-status';
import { DocumentPageViewButton } from '~/components/pages/document/document-page-view-button';
import { DocumentPageViewDropdown } from '~/components/pages/document/document-page-view-dropdown';
import { DocumentPageViewInformation } from '~/components/pages/document/document-page-view-information';
import { DocumentPageViewRecentActivity } from '~/components/pages/document/document-page-view-recent-activity';
import { DocumentPageViewRecipients } from '~/components/pages/document/document-page-view-recipients';
import { useAuth } from '~/providers/auth';
import { DocumentPageViewButton } from './document-page-view-button'; import type { Route } from './+types/$id._index';
import { DocumentPageViewDropdown } from './document-page-view-dropdown';
import { DocumentPageViewInformation } from './document-page-view-information';
import { DocumentPageViewRecentActivity } from './document-page-view-recent-activity';
import { DocumentPageViewRecipients } from './document-page-view-recipients';
export type DocumentPageViewProps = { export async function loader({ request, params }: Route.LoaderArgs) {
documentId: number; const { id } = params;
team?: Team & { teamEmail: TeamEmail | null } & { currentTeamMember: { role: TeamMemberRole } };
};
export const DocumentPageView = async ({ documentId, team }: DocumentPageViewProps) => { const { user } = await getRequiredSession(request);
const { _ } = useLingui();
// Todo: Get from parent loader, this is just for testing.
const team = await prisma.team.findFirst({
where: {
documents: {
some: {
id: Number(id),
},
},
},
});
const documentId = Number(id);
const documentRootPath = formatDocumentsPath(team?.url); const documentRootPath = formatDocumentsPath(team?.url);
const { user } = await getRequiredServerComponentSession(); if (!documentId || Number.isNaN(documentId)) {
return redirect(documentRootPath);
}
const document = await getDocumentById({ const document = await getDocumentById({
documentId, documentId,
@ -54,7 +68,7 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
}).catch(() => null); }).catch(() => null);
if (document?.teamId && !team?.url) { if (document?.teamId && !team?.url) {
redirect(documentRootPath); return redirect(documentRootPath);
} }
const documentVisibility = document?.visibility; const documentVisibility = document?.visibility;
@ -73,19 +87,15 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
.otherwise(() => false); .otherwise(() => false);
} }
const isDocumentHistoryEnabled = await getServerComponentFlag(
'app_document_page_view_history_sheet',
);
if (!document || !document.documentData || (team && !canAccessDocument)) { if (!document || !document.documentData || (team && !canAccessDocument)) {
redirect(documentRootPath); return redirect(documentRootPath);
} }
if (team && !canAccessDocument) { if (team && !canAccessDocument) {
redirect(documentRootPath); return redirect(documentRootPath);
} }
const { documentData, documentMeta } = document; const { documentMeta } = document;
if (documentMeta?.password) { if (documentMeta?.password) {
const key = DOCUMENSO_ENCRYPTION_KEY; const key = DOCUMENSO_ENCRYPTION_KEY;
@ -104,6 +114,7 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
documentMeta.password = securePassword; documentMeta.password = securePassword;
} }
// Todo: Get full document instead???
const [recipients, fields] = await Promise.all([ const [recipients, fields] = await Promise.all([
getRecipientsForDocument({ getRecipientsForDocument({
documentId, documentId,
@ -122,13 +133,30 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
recipients, recipients,
}; };
return {
document: documentWithRecipients,
documentRootPath,
fields,
};
}
export default function DocumentPage({ loaderData }: Route.ComponentProps) {
const { _ } = useLingui();
const { user } = useAuth();
const { document, documentRootPath, fields } = loaderData;
const { recipients, documentData, documentMeta } = document;
const isDocumentHistoryEnabled = false; // Todo: Was flag
return ( return (
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8"> <div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
{document.status === DocumentStatus.PENDING && ( {document.status === DocumentStatus.PENDING && (
<DocumentRecipientLinkCopyDialog recipients={recipients} /> <DocumentRecipientLinkCopyDialog recipients={recipients} />
)} )}
<Link href={documentRootPath} className="flex items-center text-[#7AC455] hover:opacity-80"> <Link to={documentRootPath} className="flex items-center text-[#7AC455] hover:opacity-80">
<ChevronLeft className="mr-2 inline-block h-5 w-5" /> <ChevronLeft className="mr-2 inline-block h-5 w-5" />
<Trans>Documents</Trans> <Trans>Documents</Trans>
</Link> </Link>
@ -207,7 +235,7 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
{_(FRIENDLY_STATUS_MAP[document.status].labelExtended)} {_(FRIENDLY_STATUS_MAP[document.status].labelExtended)}
</h3> </h3>
<DocumentPageViewDropdown document={documentWithRecipients} team={team} /> <DocumentPageViewDropdown document={document} />
</div> </div>
<p className="text-muted-foreground mt-2 px-4 text-sm"> <p className="text-muted-foreground mt-2 px-4 text-sm">
@ -235,18 +263,15 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
</p> </p>
<div className="mt-4 border-t px-4 pt-4"> <div className="mt-4 border-t px-4 pt-4">
<DocumentPageViewButton document={documentWithRecipients} team={team} /> <DocumentPageViewButton document={document} />
</div> </div>
</section> </section>
{/* Document information section. */} {/* Document information section. */}
<DocumentPageViewInformation document={documentWithRecipients} userId={user.id} /> <DocumentPageViewInformation document={document} userId={user.id} />
{/* Recipients section. */} {/* Recipients section. */}
<DocumentPageViewRecipients <DocumentPageViewRecipients document={document} documentRootPath={documentRootPath} />
document={documentWithRecipients}
documentRootPath={documentRootPath}
/>
{/* Recent activity section. */} {/* Recent activity section. */}
<DocumentPageViewRecentActivity documentId={document.id} userId={user.id} /> <DocumentPageViewRecentActivity documentId={document.id} userId={user.id} />
@ -255,4 +280,4 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
</div> </div>
</div> </div>
); );
}; }

View File

@ -1,37 +1,49 @@
import { Plural, Trans } from '@lingui/macro'; import { Plural, Trans } from '@lingui/macro';
import type { Team } from '@prisma/client';
import { TeamMemberRole } from '@prisma/client'; import { TeamMemberRole } from '@prisma/client';
import { DocumentStatus as InternalDocumentStatus } from '@prisma/client'; import { DocumentStatus as InternalDocumentStatus } from '@prisma/client';
import { ChevronLeft, Users2 } from 'lucide-react'; import { ChevronLeft, Users2 } from 'lucide-react';
import { Link, redirect } from 'react-router';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { getRequiredSession } from '@documenso/auth/server/lib/utils/get-session';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise'; import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto'; import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id'; import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility'; import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { symmetricDecrypt } from '@documenso/lib/universal/crypto'; import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip'; import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
import { DocumentStatus } from '~/components/formatter/document-status'; import { DocumentStatus } from '~/components/formatter/document-status';
import { DocumentEditForm } from '~/components/pages/document/document-edit-form';
import { EditDocumentForm } from '../edit-document'; import type { Route } from './+types/$id.edit';
export type DocumentEditPageViewProps = { export async function loader({ request, params }: Route.LoaderArgs) {
documentId: number; const { id } = params;
team?: Team & { currentTeamMember: { role: TeamMemberRole } };
}; const { user } = await getRequiredSession(request);
// Todo: Get from parent loader, this is just for testing.
const team = await prisma.team.findFirst({
where: {
documents: {
some: {
id: Number(id),
},
},
},
});
const documentId = Number(id);
export const DocumentEditPageView = async ({ documentId, team }: DocumentEditPageViewProps) => {
const documentRootPath = formatDocumentsPath(team?.url); const documentRootPath = formatDocumentsPath(team?.url);
if (!documentId || Number.isNaN(documentId)) { if (!documentId || Number.isNaN(documentId)) {
redirect(documentRootPath); return redirect(documentRootPath);
} }
const { user } = await getRequiredServerComponentSession();
const document = await getDocumentWithDetailsById({ const document = await getDocumentWithDetailsById({
documentId, documentId,
userId: user.id, userId: user.id,
@ -39,7 +51,7 @@ export const DocumentEditPageView = async ({ documentId, team }: DocumentEditPag
}).catch(() => null); }).catch(() => null);
if (document?.teamId && !team?.url) { if (document?.teamId && !team?.url) {
redirect(documentRootPath); return redirect(documentRootPath);
} }
const documentVisibility = document?.visibility; const documentVisibility = document?.visibility;
@ -59,15 +71,15 @@ export const DocumentEditPageView = async ({ documentId, team }: DocumentEditPag
} }
if (!document) { if (!document) {
redirect(documentRootPath); return redirect(documentRootPath);
} }
if (team && !canAccessDocument) { if (team && !canAccessDocument) {
redirect(documentRootPath); return redirect(documentRootPath);
} }
if (document.status === InternalDocumentStatus.COMPLETED) { if (document.status === InternalDocumentStatus.COMPLETED) {
redirect(`${documentRootPath}/${documentId}`); return redirect(`${documentRootPath}/${documentId}`);
} }
const { documentMeta, recipients } = document; const { documentMeta, recipients } = document;
@ -94,9 +106,21 @@ export const DocumentEditPageView = async ({ documentId, team }: DocumentEditPag
teamId: team?.id, teamId: team?.id,
}); });
return {
document,
documentRootPath,
isDocumentEnterprise,
};
}
export default function DocumentEditPage({ loaderData }: Route.ComponentProps) {
const { document, documentRootPath, isDocumentEnterprise } = loaderData;
const { recipients } = document;
return ( return (
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8"> <div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
<Link href={documentRootPath} className="flex items-center text-[#7AC455] hover:opacity-80"> <Link to={documentRootPath} className="flex items-center text-[#7AC455] hover:opacity-80">
<ChevronLeft className="mr-2 inline-block h-5 w-5" /> <ChevronLeft className="mr-2 inline-block h-5 w-5" />
<Trans>Documents</Trans> <Trans>Documents</Trans>
</Link> </Link>
@ -128,7 +152,7 @@ export const DocumentEditPageView = async ({ documentId, team }: DocumentEditPag
)} )}
</div> </div>
<EditDocumentForm <DocumentEditForm
className="mt-6" className="mt-6"
initialDocument={document} initialDocument={document}
documentRootPath={documentRootPath} documentRootPath={documentRootPath}
@ -136,4 +160,4 @@ export const DocumentEditPageView = async ({ documentId, team }: DocumentEditPag
/> />
</div> </div>
); );
}; }

View File

@ -4,36 +4,50 @@ import { useLingui } from '@lingui/react';
import type { Recipient } from '@prisma/client'; import type { Recipient } from '@prisma/client';
import { ChevronLeft } from 'lucide-react'; import { ChevronLeft } from 'lucide-react';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { Link, redirect } from 'react-router';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getRequiredSession } from '@documenso/auth/server/lib/utils/get-session';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document'; import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { Card } from '@documenso/ui/primitives/card'; import { Card } from '@documenso/ui/primitives/card';
import { import {
DocumentStatus as DocumentStatusComponent, DocumentStatus as DocumentStatusComponent,
FRIENDLY_STATUS_MAP, FRIENDLY_STATUS_MAP,
} from '~/components/formatter/document-status'; } from '~/components/formatter/document-status';
import { useOptionalCurrentTeam } from '~/providers/team'; import { DocumentAuditLogDownloadButton } from '~/components/pages/document/document-audit-log-download-button';
import { DocumentCertificateDownloadButton } from '~/components/pages/document/document-certificate-download-button';
import { DocumentLogsTable } from '~/components/tables/document-logs-table';
import { DocumentLogsDataTable } from './document-logs-data-table'; import type { Route } from './+types/$id.logs';
import { DownloadAuditLogButton } from './download-audit-log-button';
import { DownloadCertificateButton } from './download-certificate-button';
export type DocumentLogsPageViewProps = { export async function loader({ request, params }: Route.LoaderArgs) {
documentId: number; const { id } = params;
};
export const DocumentLogsPageView = async ({ documentId }: DocumentLogsPageViewProps) => { const { user } = await getRequiredSession(request);
const { _, i18n } = useLingui();
const team = useOptionalCurrentTeam(); // Todo: Get from parent loader, this is just for testing.
const team = await prisma.team.findFirst({
where: {
documents: {
some: {
id: Number(id),
},
},
},
});
const documentId = Number(id);
const documentRootPath = formatDocumentsPath(team?.url); const documentRootPath = formatDocumentsPath(team?.url);
const { user } = await getRequiredServerComponentSession(); if (!documentId || Number.isNaN(documentId)) {
return redirect(documentRootPath);
}
// Todo: Get detailed?
const [document, recipients] = await Promise.all([ const [document, recipients] = await Promise.all([
getDocumentById({ getDocumentById({
documentId, documentId,
@ -48,9 +62,21 @@ export const DocumentLogsPageView = async ({ documentId }: DocumentLogsPageViewP
]); ]);
if (!document || !document.documentData) { if (!document || !document.documentData) {
redirect(documentRootPath); return redirect(documentRootPath);
} }
return {
document,
documentRootPath,
recipients,
};
}
export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps) {
const { document, documentRootPath, recipients } = loaderData;
const { _, i18n } = useLingui();
const documentInformation: { description: MessageDescriptor; value: string }[] = [ const documentInformation: { description: MessageDescriptor; value: string }[] = [
{ {
description: msg`Document title`, description: msg`Document title`,
@ -97,11 +123,10 @@ export const DocumentLogsPageView = async ({ documentId }: DocumentLogsPageViewP
return `[${recipient.role}] ${text}`; return `[${recipient.role}] ${text}`;
}; };
return ( return (
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8"> <div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
<Link <Link
href={`${documentRootPath}/${document.id}`} to={`${documentRootPath}/${document.id}`}
className="flex items-center text-[#7AC455] hover:opacity-80" className="flex items-center text-[#7AC455] hover:opacity-80"
> >
<ChevronLeft className="mr-2 inline-block h-5 w-5" /> <ChevronLeft className="mr-2 inline-block h-5 w-5" />
@ -127,14 +152,13 @@ export const DocumentLogsPageView = async ({ documentId }: DocumentLogsPageViewP
</div> </div>
<div className="mt-4 flex w-full flex-row sm:mt-0 sm:w-auto sm:self-end"> <div className="mt-4 flex w-full flex-row sm:mt-0 sm:w-auto sm:self-end">
<DownloadCertificateButton <DocumentCertificateDownloadButton
className="mr-2" className="mr-2"
documentId={document.id} documentId={document.id}
documentStatus={document.status} documentStatus={document.status}
teamId={team?.id}
/> />
<DownloadAuditLogButton teamId={team?.id} documentId={document.id} /> <DocumentAuditLogDownloadButton documentId={document.id} />
</div> </div>
</div> </div>
@ -161,8 +185,8 @@ export const DocumentLogsPageView = async ({ documentId }: DocumentLogsPageViewP
</section> </section>
<section className="mt-6"> <section className="mt-6">
<DocumentLogsDataTable documentId={document.id} /> <DocumentLogsTable documentId={document.id} />
</section> </section>
</div> </div>
); );
}; }

View File

@ -0,0 +1,165 @@
import { Trans } from '@lingui/macro';
import { useSearchParams } from 'react-router';
import { Link } from 'react-router';
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { parseToIntegerArray } from '@documenso/lib/utils/params';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import { trpc } from '@documenso/trpc/react';
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { DocumentSearch } from '~/components/(dashboard)/document-search/document-search';
import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-selector';
import { isPeriodSelectorValue } from '~/components/(dashboard)/period-selector/types';
import { DocumentUploadDropzone } from '~/components/document/document-upload';
import { DocumentStatus } from '~/components/formatter/document-status';
import { UpcomingProfileClaimTeaser } from '~/components/general/upcoming-profile-claim-teaser';
import { DocumentsTable } from '~/components/tables/documents-table';
import { DocumentsTableEmptyState } from '~/components/tables/documents-table-empty-state';
import { DocumentsTableSenderFilter } from '~/components/tables/documents-table-sender-filter';
import { useAuth } from '~/providers/auth';
import { useOptionalCurrentTeam } from '~/providers/team';
export function meta() {
return [{ title: 'Documents' }];
}
// searchParams?: {
// status?: ExtendedDocumentStatus;
// period?: PeriodSelectorValue;
// page?: string;
// perPage?: string;
// senderIds?: string;
// search?: string;
// };
export default function DocumentsPage() {
const [searchParams] = useSearchParams();
const { user } = useAuth();
const team = useOptionalCurrentTeam();
const status = isExtendedDocumentStatus(searchParams.status) ? searchParams.status : 'ALL';
const period = isPeriodSelectorValue(searchParams.period) ? searchParams.period : '';
const page = Number(searchParams.page) || 1;
const perPage = Number(searchParams.perPage) || 20;
const senderIds = parseToIntegerArray(searchParams.senderIds ?? '');
const search = searchParams.search || '';
const currentTeam = team
? { id: team.id, url: team.url, teamEmail: team.teamEmail?.email }
: undefined;
const currentTeamMemberRole = team?.currentTeamMember?.role;
// const results = await findDocuments({
// status,
// orderBy: {
// column: 'createdAt',
// direction: 'desc',
// },
// page,
// perPage,
// period,
// senderIds,
// query: search,
// });
const { data, isLoading, isLoadingError } = trpc.document.findDocuments.useQuery({
page,
perPage,
});
const getTabHref = (value: typeof status) => {
const params = new URLSearchParams(searchParams);
params.set('status', value);
if (params.has('page')) {
params.delete('page');
}
return `${formatDocumentsPath(team?.url)}?${params.toString()}`;
};
return (
<>
<UpcomingProfileClaimTeaser />
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
<DocumentUploadDropzone team={currentTeam} />
<div className="mt-12 flex flex-wrap items-center justify-between gap-x-4 gap-y-8">
<div className="flex flex-row items-center">
{team && (
<Avatar className="dark:border-border mr-3 h-12 w-12 border-2 border-solid border-white">
{team.avatarImageId && <AvatarImage src={formatAvatarUrl(team.avatarImageId)} />}
<AvatarFallback className="text-xs text-gray-400">
{team.name.slice(0, 1)}
</AvatarFallback>
</Avatar>
)}
<h1 className="text-4xl font-semibold">
<Trans>Documents</Trans>
</h1>
</div>
<div className="-m-1 flex flex-wrap gap-x-4 gap-y-6 overflow-hidden p-1">
<Tabs value={status} className="overflow-x-auto">
<TabsList>
{[
ExtendedDocumentStatus.INBOX,
ExtendedDocumentStatus.PENDING,
ExtendedDocumentStatus.COMPLETED,
ExtendedDocumentStatus.DRAFT,
ExtendedDocumentStatus.ALL,
].map((value) => (
<TabsTrigger
key={value}
className="hover:text-foreground min-w-[60px]"
value={value}
asChild
>
<Link to={getTabHref(value)} preventScrollReset>
<DocumentStatus status={value} />
{value !== ExtendedDocumentStatus.ALL && (
<span className="ml-1 inline-block opacity-50">todo</span>
)}
</Link>
</TabsTrigger>
))}
</TabsList>
</Tabs>
{team && <DocumentsTableSenderFilter teamId={team.id} />}
<div className="flex w-48 flex-wrap items-center justify-between gap-x-2 gap-y-4">
<PeriodSelector />
</div>
<div className="flex w-48 flex-wrap items-center justify-between gap-x-2 gap-y-4">
<DocumentSearch initialValue={search} />
</div>
</div>
</div>
<div className="mt-8">
<div>
{data && data.count === 0 ? (
<DocumentsTableEmptyState status={status} />
) : (
<DocumentsTable
data={data}
isLoading={isLoading}
isLoadingError={isLoadingError}
showSenderColumn={team !== undefined}
team={currentTeam}
/>
)}
</div>
</div>
</div>
</>
);
}

View File

@ -1,19 +0,0 @@
import { useSearchParams } from 'react-router';
import { UpcomingProfileClaimTeaser } from '~/components/general/upcoming-profile-claim-teaser';
import { DocumentsPageView } from '~/documents+/_documents-page-view';
export function meta() {
return [{ title: 'Documents' }];
}
export default function DocumentsPage() {
const [searchParams] = useSearchParams();
return (
<>
<UpcomingProfileClaimTeaser />
<DocumentsPageView searchParams={searchParams} />
</>
);
}

View File

@ -2,14 +2,11 @@ import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header'; import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
import { AccountDeleteDialog } from '~/components/dialogs/account-delete-dialog';
import { AvatarImageForm } from '~/components/forms/avatar-image'; import { AvatarImageForm } from '~/components/forms/avatar-image';
import { ProfileForm } from '~/components/forms/profile'; import { ProfileForm } from '~/components/forms/profile';
import type { Route } from './+types/profile'; export function meta() {
// import { DeleteAccountDialog } from './settings/profile/delete-account-dialog';
export function meta(_args: Route.MetaArgs) {
return [{ title: 'Profile' }]; return [{ title: 'Profile' }];
} }
@ -28,7 +25,7 @@ export default function SettingsProfile() {
<hr className="my-4 max-w-xl" /> <hr className="my-4 max-w-xl" />
{/* <DeleteAccountDialog className="max-w-xl" /> */} <AccountDeleteDialog className="max-w-xl" />
</div> </div>
); );
} }

View File

@ -6,8 +6,8 @@ import { trpc } from '@documenso/trpc/react';
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out'; import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header'; import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
import { CreateTeamDialog } from '~/components/(teams)/dialogs/create-team-dialog';
import { UserSettingsTeamsPageDataTable } from '~/components/(teams)/tables/user-settings-teams-page-data-table'; import { UserSettingsTeamsPageDataTable } from '~/components/(teams)/tables/user-settings-teams-page-data-table';
import { TeamCreateDialog } from '~/components/dialogs/team-create-dialog';
import { TeamEmailUsage } from './team-email-usage'; import { TeamEmailUsage } from './team-email-usage';
import { TeamInvitations } from './team-invitations'; import { TeamInvitations } from './team-invitations';
@ -23,7 +23,7 @@ export default function TeamsSettingsPage() {
title={_(msg`Teams`)} title={_(msg`Teams`)}
subtitle={_(msg`Manage all teams you are currently associated with.`)} subtitle={_(msg`Manage all teams you are currently associated with.`)}
> >
<CreateTeamDialog /> <TeamCreateDialog />
</SettingsHeader> </SettingsHeader>
<UserSettingsTeamsPageDataTable /> <UserSettingsTeamsPageDataTable />

View File

@ -0,0 +1,65 @@
import { Outlet, replace } from 'react-router';
import { getRequiredSession } from '@documenso/auth/server/lib/utils/get-session';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { getTeams } from '@documenso/lib/server-only/team/get-teams';
import { TrpcProvider } from '@documenso/trpc/react';
import { TeamProvider } from '~/providers/team';
import type { Route } from './+types/_layout';
export const loader = async ({ request, params }: Route.LoaderArgs) => {
// Todo: get user better from context or something
// Todo: get user better from context or something
const { user } = await getRequiredSession(request);
const [getTeamsPromise, getTeamPromise] = await Promise.allSettled([
getTeams({ userId: user.id }),
getTeamByUrl({ userId: user.id, teamUrl: params.teamUrl }),
]);
console.log('1');
console.log({ userId: user.id, teamUrl: params.teamUrl });
console.log(getTeamPromise.status);
if (getTeamPromise.status === 'rejected') {
console.log('2');
return replace('/documents');
}
const team = getTeamPromise.value;
const teams = getTeamsPromise.status === 'fulfilled' ? getTeamsPromise.value : [];
const trpcHeaders = {
'x-team-Id': team.id.toString(),
};
return {
team,
teams,
trpcHeaders,
};
};
export default function Layout({ loaderData }: Route.ComponentProps) {
const { team, trpcHeaders } = loaderData;
return (
<TeamProvider team={team}>
<TrpcProvider headers={trpcHeaders}>
{/* Todo: Do this. */}
{/* {team.subscription && team.subscription.status !== SubscriptionStatus.ACTIVE && (
<LayoutBillingBanner
subscription={team.subscription}
teamId={team.id}
userRole={team.currentTeamMember.role}
/>
)} */}
<main className="mt-8 pb-8 md:mt-12 md:pb-12">
<Outlet />
</main>
</TrpcProvider>
</TeamProvider>
);
}

View File

@ -0,0 +1,5 @@
import DocumentPage, { loader } from '~/routes/_authenticated+/documents+/$id._index';
export { loader };
export default DocumentPage;

View File

@ -0,0 +1,5 @@
import DocumentEditPage, { loader } from '~/routes/_authenticated+/documents+/$id.edit';
export { loader };
export default DocumentEditPage;

View File

@ -0,0 +1,5 @@
import DocumentLogsPage, { loader } from '~/routes/_authenticated+/documents+/$id.logs';
export { loader };
export default DocumentLogsPage;

View File

@ -0,0 +1,5 @@
import DocumentsPage, { meta } from '~/routes/_authenticated+/documents+/_index';
export { meta };
export default DocumentsPage;

View File

@ -0,0 +1,5 @@
import DocumentsPage, { meta } from '~/routes/_authenticated+/documents+/_index';
export { meta };
export default DocumentsPage;

View File

@ -1,23 +1,24 @@
import { Trans } from '@lingui/macro'; import { Trans } from '@lingui/macro';
import { Link, redirect } from 'react-router'; import { Link, redirect } from 'react-router';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { import {
IS_GOOGLE_SSO_ENABLED, IS_GOOGLE_SSO_ENABLED,
IS_OIDC_SSO_ENABLED, IS_OIDC_SSO_ENABLED,
OIDC_PROVIDER_LABEL, OIDC_PROVIDER_LABEL,
} from '@documenso/lib/constants/auth'; } from '@documenso/lib/constants/auth';
import { env } from '@documenso/lib/utils/env';
import { SignInForm } from '~/components/forms/signin'; import { SignInForm } from '~/components/forms/signin';
import type { Route } from './+types/signin'; import type { Route } from './+types/signin';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
export function meta(_args: Route.MetaArgs) { export function meta(_args: Route.MetaArgs) {
return [{ title: 'Sign In' }]; return [{ title: 'Sign In' }];
} }
export async function loader({ request }: Route.LoaderArgs) { export async function loader({ request }: Route.LoaderArgs) {
const session = await getSession(request) const session = await getSession(request);
if (session.isAuthenticated) { if (session.isAuthenticated) {
return redirect('/documents'); return redirect('/documents');
@ -25,9 +26,7 @@ export async function loader({ request }: Route.LoaderArgs) {
} }
export default function SignIn() { export default function SignIn() {
// Todo const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
// const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
const NEXT_PUBLIC_DISABLE_SIGNUP = 'false';
return ( return (
<div className="w-screen max-w-lg px-4"> <div className="w-screen max-w-lg px-4">

View File

@ -1,19 +1,16 @@
import { redirect } from 'react-router'; import { redirect } from 'react-router';
import { IS_GOOGLE_SSO_ENABLED, IS_OIDC_SSO_ENABLED } from '@documenso/lib/constants/auth'; import { IS_GOOGLE_SSO_ENABLED, IS_OIDC_SSO_ENABLED } from '@documenso/lib/constants/auth';
import { env } from '@documenso/lib/utils/env';
import { SignUpForm } from '~/components/forms/signup'; import { SignUpForm } from '~/components/forms/signup';
import type { Route } from './+types/_unauth.signup'; export function meta() {
export function meta(_args: Route.MetaArgs) {
return [{ title: 'Sign Up' }]; return [{ title: 'Sign Up' }];
} }
export function loader() { export function loader() {
// Todo const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
// const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
const NEXT_PUBLIC_DISABLE_SIGNUP: string = 'false';
if (NEXT_PUBLIC_DISABLE_SIGNUP === 'true') { if (NEXT_PUBLIC_DISABLE_SIGNUP === 'true') {
return redirect('/signin'); return redirect('/signin');

View File

@ -7,7 +7,8 @@
"dev": "react-router dev", "dev": "react-router dev",
"start": "cross-env NODE_ENV=production node dist/server/index.js", "start": "cross-env NODE_ENV=production node dist/server/index.js",
"clean": "rimraf .react-router && rimraf node_modules", "clean": "rimraf .react-router && rimraf node_modules",
"typecheck": "react-router typegen && tsc" "typecheck": "react-router typegen && tsc",
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
}, },
"dependencies": { "dependencies": {
"@epic-web/remember": "^1.1.0", "@epic-web/remember": "^1.1.0",
@ -48,4 +49,4 @@
"vite-plugin-babel-macros": "^1.0.6", "vite-plugin-babel-macros": "^1.0.6",
"vite-tsconfig-paths": "^5.1.4" "vite-tsconfig-paths": "^5.1.4"
} }
} }

22
apps/remix/public/pdf.worker.min.js vendored Normal file

File diff suppressed because one or more lines are too long

10
apps/remix/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string;
// more env variables...
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@ -4,15 +4,18 @@ import autoprefixer from 'autoprefixer';
import serverAdapter from 'hono-react-router-adapter/vite'; import serverAdapter from 'hono-react-router-adapter/vite';
import path from 'path'; import path from 'path';
import tailwindcss from 'tailwindcss'; import tailwindcss from 'tailwindcss';
import { defineConfig } from 'vite'; import { defineConfig, loadEnv } from 'vite';
import macrosPlugin from 'vite-plugin-babel-macros'; import macrosPlugin from 'vite-plugin-babel-macros';
import tsconfigPaths from 'vite-tsconfig-paths'; import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({ export default defineConfig({
envDir: path.join(__dirname, '../../'), envDir: path.join(__dirname, '../../'),
envPrefix: 'NEXT_', envPrefix: '__DO_NOT_USE_OR_YOU_WILL_BE_FIRED__',
define: { define: {
'process.env': {}, 'process.env': {
...process.env,
...loadEnv('development', path.join(__dirname, '../../'), ''),
},
}, },
css: { css: {
postcss: { postcss: {
@ -20,6 +23,7 @@ export default defineConfig({
}, },
}, },
ssr: { ssr: {
// , 'next/font/google' doesnot work
noExternal: ['react-dropzone', 'recharts'], noExternal: ['react-dropzone', 'recharts'],
}, },
server: { server: {
@ -36,7 +40,7 @@ export default defineConfig({
tsconfigPaths(), tsconfigPaths(),
], ],
// optimizeDeps: { // optimizeDeps: {
// exclude: ['@node-rs/bcrypt', '@node-rs/bcrypt-wasm32-wasi', 'react-dropzone', '@documenso/ui'], // Todo: Probably remove. // exclude: ['next/font/google'], // Todo: Probably remove.
// force: true, // force: true,
// }, // },
}); });

View File

@ -10,7 +10,6 @@ import { z } from 'zod';
import { downloadFile } from '@documenso/lib/client-only/download-file'; import { downloadFile } from '@documenso/lib/client-only/download-file';
import { AppError } from '@documenso/lib/errors/app-error'; import { AppError } from '@documenso/lib/errors/app-error';
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -147,7 +146,7 @@ export const ViewRecoveryCodesDialog = () => {
<Alert variant="destructive"> <Alert variant="destructive">
<AlertDescription> <AlertDescription>
{match(AppError.parseError(error).message) {match(AppError.parseError(error).message)
.with(ErrorCode.INCORRECT_TWO_FACTOR_CODE, () => ( .with('INCORRECT_TWO_FACTOR_CODE', () => (
<Trans>Invalid code. Please try again.</Trans> <Trans>Invalid code. Please try again.</Trans>
)) ))
.otherwise(() => ( .otherwise(() => (

View File

@ -76,10 +76,10 @@ export const ZSignUpFormV2Schema = z
export const signupErrorMessages: Record<string, MessageDescriptor> = { export const signupErrorMessages: Record<string, MessageDescriptor> = {
SIGNUP_DISABLED: msg`Signups are disabled.`, SIGNUP_DISABLED: msg`Signups are disabled.`,
PROFILE_URL_TAKEN: msg`This username has already been taken`,
PREMIUM_PROFILE_URL: msg`Only subscribers can have a username shorter than 6 characters`,
[AppErrorCode.ALREADY_EXISTS]: msg`User with this email already exists. Please use a different email address.`, [AppErrorCode.ALREADY_EXISTS]: msg`User with this email already exists. Please use a different email address.`,
[AppErrorCode.INVALID_REQUEST]: msg`We were unable to create your account. Please review the information you provided and try again.`, [AppErrorCode.INVALID_REQUEST]: msg`We were unable to create your account. Please review the information you provided and try again.`,
[AppErrorCode.PROFILE_URL_TAKEN]: msg`This username has already been taken`,
[AppErrorCode.PREMIUM_PROFILE_URL]: msg`Only subscribers can have a username shorter than 6 characters`,
}; };
export type TSignUpFormV2Schema = z.infer<typeof ZSignUpFormV2Schema>; export type TSignUpFormV2Schema = z.infer<typeof ZSignUpFormV2Schema>;

View File

@ -7,8 +7,8 @@ import type { AuthAppType } from '../server';
import type { import type {
TForgotPasswordSchema, TForgotPasswordSchema,
TResetPasswordSchema, TResetPasswordSchema,
TSignInFormSchema, TSignInSchema,
TSignUpRequestSchema, TSignUpSchema,
TVerifyEmailSchema, TVerifyEmailSchema,
} from '../server/types/email-password'; } from '../server/types/email-password';
import type { TPasskeyAuthorizeSchema } from '../server/types/passkey'; import type { TPasskeyAuthorizeSchema } from '../server/types/passkey';
@ -32,18 +32,6 @@ export class AuthClient {
return this.client.session.$get(); return this.client.session.$get();
} }
public passkey = {
signIn: async (data: TPasskeyAuthorizeSchema) => {
const result = await this.client['passkey'].authorize.$post({ json: data });
if (result.ok) {
return result.json();
}
throw new Error(result.statusText);
},
};
private async handleResponse<T>(response: ClientResponse<T>) { private async handleResponse<T>(response: ClientResponse<T>) {
if (!response.ok) { if (!response.ok) {
const error = await response.json(); const error = await response.json();
@ -59,9 +47,16 @@ export class AuthClient {
} }
public emailPassword = { public emailPassword = {
signIn: async (data: TSignInFormSchema) => { signIn: async (data: TSignInSchema & { redirectUrl?: string }) => {
const response = await this.client['email-password'].authorize.$post({ json: data }); const response = await this.client['email-password'].authorize
return this.handleResponse(response); .$post({ json: data })
.then(this.handleResponse);
if (data.redirectUrl) {
window.location.href = data.redirectUrl;
}
return response;
}, },
forgotPassword: async (data: TForgotPasswordSchema) => { forgotPassword: async (data: TForgotPasswordSchema) => {
@ -74,7 +69,7 @@ export class AuthClient {
return this.handleResponse(response); return this.handleResponse(response);
}, },
signUp: async (data: TSignUpRequestSchema) => { signUp: async (data: TSignUpSchema) => {
const response = await this.client['email-password']['signup'].$post({ json: data }); const response = await this.client['email-password']['signup'].$post({ json: data });
return this.handleResponse(response); return this.handleResponse(response);
}, },
@ -85,20 +80,29 @@ export class AuthClient {
}, },
}; };
public google = { public passkey = {
signIn: async () => { signIn: async (data: TPasskeyAuthorizeSchema & { redirectUrl?: string }) => {
const response = await this.client['google'].authorize.$post(); const response = await this.client['passkey'].authorize
.$post({ json: data })
.then(this.handleResponse);
// const parsedResponse = this.handleResponse(response); if (data.redirectUrl) {
if (!response.ok) { window.location.href = data.redirectUrl;
const error = await response.json();
throw AppError.parseError(error);
} }
const test = await response.json(); return response;
},
};
window.location.href = test.redirectUrl; public google = {
signIn: async () => {
const response = await this.client['google'].authorize.$post().then(this.handleResponse);
if (response.redirectUrl) {
window.location.href = response.redirectUrl;
}
return response;
}, },
}; };
} }

View File

@ -1,5 +1,8 @@
import { env } from '@documenso/lib/utils/env';
// Todo: Delete
export const authDebugger = (message: string) => { export const authDebugger = (message: string) => {
if (process.env.NODE_ENV === 'development') { if (env('NODE_ENV') === 'development') {
console.log(`[DEBUG]: ${message}`); console.log(`[DEBUG]: ${message}`);
} }
}; };

View File

@ -4,6 +4,7 @@ import { Hono } from 'hono';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { z } from 'zod'; import { z } from 'zod';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { AppError } from '@documenso/lib/errors/app-error'; import { AppError } from '@documenso/lib/errors/app-error';
import { jobsClient } from '@documenso/lib/jobs/client'; import { jobsClient } from '@documenso/lib/jobs/client';
import { disableTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/disable-2fa'; import { disableTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/disable-2fa';
@ -16,19 +17,25 @@ import { createUser } from '@documenso/lib/server-only/user/create-user';
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password'; import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
import { getMostRecentVerificationTokenByUserId } from '@documenso/lib/server-only/user/get-most-recent-verification-token-by-user-id'; import { getMostRecentVerificationTokenByUserId } from '@documenso/lib/server-only/user/get-most-recent-verification-token-by-user-id';
import { resetPassword } from '@documenso/lib/server-only/user/reset-password'; import { resetPassword } from '@documenso/lib/server-only/user/reset-password';
import { verifyEmail } from '@documenso/lib/server-only/user/verify-email'; import { updatePassword } from '@documenso/lib/server-only/user/update-password';
import {
EMAIL_VERIFICATION_STATE,
verifyEmail,
} from '@documenso/lib/server-only/user/verify-email';
import { env } from '@documenso/lib/utils/env';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { UserSecurityAuditLogType } from '@documenso/prisma/client'; import { UserSecurityAuditLogType } from '@documenso/prisma/client';
import { AuthenticationErrorCode } from '../lib/errors/error-codes'; import { AuthenticationErrorCode } from '../lib/errors/error-codes';
import { onAuthorize } from '../lib/utils/authorizer'; import { onAuthorize } from '../lib/utils/authorizer';
import { getRequiredSession } from '../lib/utils/get-session'; import { getRequiredSession, getSession } from '../lib/utils/get-session';
import type { HonoAuthContext } from '../types/context'; import type { HonoAuthContext } from '../types/context';
import { import {
ZForgotPasswordSchema, ZForgotPasswordSchema,
ZResetPasswordSchema, ZResetPasswordSchema,
ZSignInFormSchema, ZSignInSchema,
ZSignUpRequestSchema, ZSignUpSchema,
ZUpdatePasswordSchema,
ZVerifyEmailSchema, ZVerifyEmailSchema,
} from '../types/email-password'; } from '../types/email-password';
@ -36,7 +43,7 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
/** /**
* Authorize endpoint. * Authorize endpoint.
*/ */
.post('/authorize', zValidator('json', ZSignInFormSchema), async (c) => { .post('/authorize', zValidator('json', ZSignInSchema), async (c) => {
const requestMetadata = c.get('requestMetadata'); const requestMetadata = c.get('requestMetadata');
const { email, password, totpCode, backupCode } = c.req.valid('json'); const { email, password, totpCode, backupCode } = c.req.valid('json');
@ -48,8 +55,8 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
}); });
if (!user || !user.password) { if (!user || !user.password) {
throw new AppError(AuthenticationErrorCode.NotFound, { throw new AppError(AuthenticationErrorCode.InvalidCredentials, {
message: 'User not found', message: 'Invalid email or password',
}); });
} }
@ -85,7 +92,7 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
}, },
}); });
throw new AppError(AuthenticationErrorCode.IncorrectTwoFactorCode); throw new AppError(AuthenticationErrorCode.InvalidTwoFactorCode);
} }
} }
@ -125,20 +132,20 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
/** /**
* Signup endpoint. * Signup endpoint.
*/ */
.post('/signup', zValidator('json', ZSignUpRequestSchema), async (c) => { .post('/signup', zValidator('json', ZSignUpSchema), async (c) => {
// if (NEXT_PUBLIC_DISABLE_SIGNUP() === 'true') { if (env('NEXT_PUBLIC_DISABLE_SIGNUP') === 'true') {
// throw new AppError('SIGNUP_DISABLED', { throw new AppError('SIGNUP_DISABLED', {
// message: 'Signups are disabled.', message: 'Signups are disabled.',
// }); });
// } }
const { name, email, password, signature, url } = c.req.valid('json'); const { name, email, password, signature, url } = c.req.valid('json');
// if (IS_BILLING_ENABLED() && url && url.length < 6) { if (IS_BILLING_ENABLED() && url && url.length < 6) {
// throw new AppError(AppErrorCode.PREMIUM_PROFILE_URL, { throw new AppError('PREMIUM_PROFILE_URL', {
// message: 'Only subscribers can have a username shorter than 6 characters', message: 'Only subscribers can have a username shorter than 6 characters',
// }); });
// } }
const user = await createUser({ name, email, password, signature, url }); const user = await createUser({ name, email, password, signature, url });
@ -149,18 +156,59 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
}, },
}); });
// Todo: Check this. return c.text('OK', 201);
return c.json({ })
user, /**
* Update password endpoint.
*/
.post('/update-password', zValidator('json', ZUpdatePasswordSchema), async (c) => {
const { password, currentPassword } = c.req.valid('json');
const requestMetadata = c.get('requestMetadata');
const session = await getSession(c);
if (!session.isAuthenticated) {
throw new AppError(AuthenticationErrorCode.Unauthorized);
}
await updatePassword({
userId: session.user.id,
password,
currentPassword,
requestMetadata,
}); });
return c.text('OK', 201);
}) })
/** /**
* Verify email endpoint. * Verify email endpoint.
*/ */
.post('/verify-email', zValidator('json', ZVerifyEmailSchema), async (c) => { .post('/verify-email', zValidator('json', ZVerifyEmailSchema), async (c) => {
await verifyEmail({ token: c.req.valid('json').token }); const { state, userId } = await verifyEmail({ token: c.req.valid('json').token });
return c.text('OK', 201); // If email is verified, automatically authenticate user.
if (state === EMAIL_VERIFICATION_STATE.VERIFIED && userId !== null) {
await onAuthorize({ userId }, c);
}
return c.json({
state,
});
})
/**
* Resend verification email endpoint.
*/
.post('/resend-email', zValidator('json', ZVerifyEmailSchema), async (c) => {
const { state, userId } = await verifyEmail({ token: c.req.valid('json').token });
// If email is verified, automatically authenticate user.
if (state === EMAIL_VERIFICATION_STATE.VERIFIED && userId !== null) {
await onAuthorize({ userId }, c);
}
return c.json({
state,
});
}) })
/** /**
* Forgot password endpoint. * Forgot password endpoint.
@ -180,9 +228,12 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
.post('/reset-password', zValidator('json', ZResetPasswordSchema), async (c) => { .post('/reset-password', zValidator('json', ZResetPasswordSchema), async (c) => {
const { token, password } = c.req.valid('json'); const { token, password } = c.req.valid('json');
const requestMetadata = c.get('requestMetadata');
await resetPassword({ await resetPassword({
token, token,
password, password,
requestMetadata,
}); });
return c.text('OK', 201); return c.text('OK', 201);
@ -291,9 +342,7 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
requestMetadata, requestMetadata,
}); });
return c.json({ return c.text('OK', 201);
success: true,
});
}, },
) )
/** /**

View File

@ -4,6 +4,7 @@ import { getCookie, setCookie } from 'hono/cookie';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { setupTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/setup-2fa'; import { setupTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/setup-2fa';
import { env } from '@documenso/lib/utils/env';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { AuthenticationErrorCode } from '../lib/errors/error-codes'; import { AuthenticationErrorCode } from '../lib/errors/error-codes';
@ -12,8 +13,8 @@ import { getRequiredSession } from '../lib/utils/get-session';
import type { HonoAuthContext } from '../types/context'; import type { HonoAuthContext } from '../types/context';
const options = { const options = {
clientId: import.meta.env.NEXT_PRIVATE_GOOGLE_CLIENT_ID, clientId: env('NEXT_PRIVATE_GOOGLE_CLIENT_ID'),
clientSecret: import.meta.env.NEXT_PRIVATE_GOOGLE_CLIENT_SECRET, clientSecret: env('NEXT_PRIVATE_GOOGLE_CLIENT_SECRET'),
redirectUri: 'http://localhost:3000/api/auth/google/callback', redirectUri: 'http://localhost:3000/api/auth/google/callback',
scope: ['openid', 'email', 'profile'], scope: ['openid', 'email', 'profile'],
id: 'google', id: 'google',
@ -36,7 +37,7 @@ export const googleRoute = new Hono<HonoAuthContext>()
setCookie(c, 'google_oauth_state', state, { setCookie(c, 'google_oauth_state', state, {
path: '/', path: '/',
httpOnly: true, httpOnly: true,
secure: process.env.NODE_ENV === 'production', secure: env('NODE_ENV') === 'production',
maxAge: 60 * 10, // 10 minutes maxAge: 60 * 10, // 10 minutes
sameSite: 'lax', sameSite: 'lax',
}); });
@ -44,7 +45,8 @@ export const googleRoute = new Hono<HonoAuthContext>()
setCookie(c, 'google_code_verifier', codeVerifier, { setCookie(c, 'google_code_verifier', codeVerifier, {
path: '/', path: '/',
httpOnly: true, httpOnly: true,
secure: process.env.NODE_ENV === 'production', // Todo: Might not be node_env but something vite specific?
secure: env('NODE_ENV') === 'production',
maxAge: 60 * 10, // 10 minutes maxAge: 60 * 10, // 10 minutes
sameSite: 'lax', sameSite: 'lax',
}); });

View File

@ -5,14 +5,14 @@ export const ZCurrentPasswordSchema = z
.min(6, { message: 'Must be at least 6 characters in length' }) .min(6, { message: 'Must be at least 6 characters in length' })
.max(72); .max(72);
export const ZSignInFormSchema = z.object({ export const ZSignInSchema = z.object({
email: z.string().email().min(1), email: z.string().email().min(1),
password: ZCurrentPasswordSchema, password: ZCurrentPasswordSchema,
totpCode: z.string().trim().optional(), totpCode: z.string().trim().optional(),
backupCode: z.string().trim().optional(), backupCode: z.string().trim().optional(),
}); });
export type TSignInFormSchema = z.infer<typeof ZSignInFormSchema>; export type TSignInSchema = z.infer<typeof ZSignInSchema>;
export const ZPasswordSchema = z export const ZPasswordSchema = z
.string() .string()
@ -31,7 +31,7 @@ export const ZPasswordSchema = z
message: 'One special character is required', message: 'One special character is required',
}); });
export const ZSignUpRequestSchema = z.object({ export const ZSignUpSchema = z.object({
name: z.string().min(1), name: z.string().min(1),
email: z.string().email(), email: z.string().email(),
password: ZPasswordSchema, password: ZPasswordSchema,
@ -47,7 +47,7 @@ export const ZSignUpRequestSchema = z.object({
.optional(), .optional(),
}); });
export type TSignUpRequestSchema = z.infer<typeof ZSignUpRequestSchema>; export type TSignUpSchema = z.infer<typeof ZSignUpSchema>;
export const ZForgotPasswordSchema = z.object({ export const ZForgotPasswordSchema = z.object({
email: z.string().email().min(1), email: z.string().email().min(1),
@ -67,3 +67,10 @@ export const ZVerifyEmailSchema = z.object({
}); });
export type TVerifyEmailSchema = z.infer<typeof ZVerifyEmailSchema>; export type TVerifyEmailSchema = z.infer<typeof ZVerifyEmailSchema>;
export const ZUpdatePasswordSchema = z.object({
currentPassword: ZCurrentPasswordSchema,
password: ZPasswordSchema,
});
export type TUpdatePasswordSchema = z.infer<typeof ZUpdatePasswordSchema>;

View File

@ -8,6 +8,7 @@ import type { Stripe } from '@documenso/lib/server-only/stripe';
import { stripe } from '@documenso/lib/server-only/stripe'; import { stripe } from '@documenso/lib/server-only/stripe';
import { createTeamFromPendingTeam } from '@documenso/lib/server-only/team/create-team'; import { createTeamFromPendingTeam } from '@documenso/lib/server-only/team/create-team';
import { getFlag } from '@documenso/lib/universal/get-feature-flag'; import { getFlag } from '@documenso/lib/universal/get-feature-flag';
import { env } from '@documenso/lib/utils/env';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { onSubscriptionDeleted } from './on-subscription-deleted'; import { onSubscriptionDeleted } from './on-subscription-deleted';
@ -47,7 +48,7 @@ export const stripeWebhookHandler = async (
const event = stripe.webhooks.constructEvent( const event = stripe.webhooks.constructEvent(
body, body,
signature, signature,
process.env.NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET, env('NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET'), // Todo: Test
); );
await match(event.type) await match(event.type)

View File

@ -1,6 +1,7 @@
import type { Transporter } from 'nodemailer'; import type { Transporter } from 'nodemailer';
import { createTransport } from 'nodemailer'; import { createTransport } from 'nodemailer';
import { env } from '@documenso/lib/utils/env';
import { ResendTransport } from '@documenso/nodemailer-resend'; import { ResendTransport } from '@documenso/nodemailer-resend';
import { MailChannelsTransport } from './transports/mailchannels'; import { MailChannelsTransport } from './transports/mailchannels';
@ -51,13 +52,13 @@ import { MailChannelsTransport } from './transports/mailchannels';
* - `NEXT_PRIVATE_SMTP_SERVICE` is optional and used specifically for well-known services like Gmail. * - `NEXT_PRIVATE_SMTP_SERVICE` is optional and used specifically for well-known services like Gmail.
*/ */
const getTransport = (): Transporter => { const getTransport = (): Transporter => {
const transport = process.env.NEXT_PRIVATE_SMTP_TRANSPORT ?? 'smtp-auth'; const transport = env('NEXT_PRIVATE_SMTP_TRANSPORT') ?? 'smtp-auth';
if (transport === 'mailchannels') { if (transport === 'mailchannels') {
return createTransport( return createTransport(
MailChannelsTransport.makeTransport({ MailChannelsTransport.makeTransport({
apiKey: process.env.NEXT_PRIVATE_MAILCHANNELS_API_KEY, apiKey: env('NEXT_PRIVATE_MAILCHANNELS_API_KEY'),
endpoint: process.env.NEXT_PRIVATE_MAILCHANNELS_ENDPOINT, endpoint: env('NEXT_PRIVATE_MAILCHANNELS_ENDPOINT'),
}), }),
); );
} }
@ -65,43 +66,41 @@ const getTransport = (): Transporter => {
if (transport === 'resend') { if (transport === 'resend') {
return createTransport( return createTransport(
ResendTransport.makeTransport({ ResendTransport.makeTransport({
apiKey: process.env.NEXT_PRIVATE_RESEND_API_KEY || '', apiKey: env('NEXT_PRIVATE_RESEND_API_KEY') || '',
}), }),
); );
} }
if (transport === 'smtp-api') { if (transport === 'smtp-api') {
if (!process.env.NEXT_PRIVATE_SMTP_HOST || !process.env.NEXT_PRIVATE_SMTP_APIKEY) { if (!env('NEXT_PRIVATE_SMTP_HOST') || !env('NEXT_PRIVATE_SMTP_APIKEY')) {
throw new Error( throw new Error(
'SMTP API transport requires NEXT_PRIVATE_SMTP_HOST and NEXT_PRIVATE_SMTP_APIKEY', 'SMTP API transport requires NEXT_PRIVATE_SMTP_HOST and NEXT_PRIVATE_SMTP_APIKEY',
); );
} }
return createTransport({ return createTransport({
host: process.env.NEXT_PRIVATE_SMTP_HOST, host: env('NEXT_PRIVATE_SMTP_HOST'),
port: Number(process.env.NEXT_PRIVATE_SMTP_PORT) || 587, port: Number(env('NEXT_PRIVATE_SMTP_PORT')) || 587,
secure: process.env.NEXT_PRIVATE_SMTP_SECURE === 'true', secure: env('NEXT_PRIVATE_SMTP_SECURE') === 'true',
auth: { auth: {
user: process.env.NEXT_PRIVATE_SMTP_APIKEY_USER ?? 'apikey', user: env('NEXT_PRIVATE_SMTP_APIKEY_USER') ?? 'apikey',
pass: process.env.NEXT_PRIVATE_SMTP_APIKEY ?? '', pass: env('NEXT_PRIVATE_SMTP_APIKEY') ?? '',
}, },
}); });
} }
return createTransport({ return createTransport({
host: process.env.NEXT_PRIVATE_SMTP_HOST ?? '127.0.0.1:2500', host: env('NEXT_PRIVATE_SMTP_HOST') ?? '127.0.0.1:2500',
port: Number(process.env.NEXT_PRIVATE_SMTP_PORT) || 587, port: Number(env('NEXT_PRIVATE_SMTP_PORT')) || 587,
secure: process.env.NEXT_PRIVATE_SMTP_SECURE === 'true', secure: env('NEXT_PRIVATE_SMTP_SECURE') === 'true',
ignoreTLS: process.env.NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS === 'true', ignoreTLS: env('NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS') === 'true',
auth: process.env.NEXT_PRIVATE_SMTP_USERNAME auth: env('NEXT_PRIVATE_SMTP_USERNAME')
? { ? {
user: process.env.NEXT_PRIVATE_SMTP_USERNAME, user: env('NEXT_PRIVATE_SMTP_USERNAME'),
pass: process.env.NEXT_PRIVATE_SMTP_PASSWORD ?? '', pass: env('NEXT_PRIVATE_SMTP_PASSWORD') ?? '',
} }
: undefined, : undefined,
...(process.env.NEXT_PRIVATE_SMTP_SERVICE ...(env('NEXT_PRIVATE_SMTP_SERVICE') ? { service: env('NEXT_PRIVATE_SMTP_SERVICE') } : {}),
? { service: process.env.NEXT_PRIVATE_SMTP_SERVICE }
: {}),
}); });
}; };

View File

@ -1,5 +1,6 @@
import { Trans } from '@lingui/macro'; import { Trans } from '@lingui/macro';
import { env } from 'next-runtime-env';
import { env } from '@documenso/lib/utils/env';
import { Button, Column, Img, Link, Section, Text } from '../components'; import { Button, Column, Img, Link, Section, Text } from '../components';
import { TemplateDocumentImage } from './template-document-image'; import { TemplateDocumentImage } from './template-document-image';

View File

@ -1,5 +1,6 @@
import { Trans } from '@lingui/macro'; import { Trans } from '@lingui/macro';
import { env } from 'next-runtime-env';
import { env } from '@documenso/lib/utils/env';
import { Button, Section, Text } from '../components'; import { Button, Section, Text } from '../components';
import { TemplateDocumentImage } from './template-document-image'; import { TemplateDocumentImage } from './template-document-image';

View File

@ -1,7 +1,9 @@
import { SentMessageInfo, Transport } from 'nodemailer'; import type { SentMessageInfo, Transport } from 'nodemailer';
import type { Address } from 'nodemailer/lib/mailer'; import type { Address } from 'nodemailer/lib/mailer';
import type MailMessage from 'nodemailer/lib/mailer/mail-message'; import type MailMessage from 'nodemailer/lib/mailer/mail-message';
import { env } from '@documenso/lib/utils/env';
const VERSION = '1.0.0'; const VERSION = '1.0.0';
type NodeMailerAddress = string | Address | Array<string | Address> | undefined; type NodeMailerAddress = string | Address | Array<string | Address> | undefined;
@ -79,9 +81,9 @@ export class MailChannelsTransport implements Transport<SentMessageInfo> {
to: mailTo, to: mailTo,
cc: mailCc.length > 0 ? mailCc : undefined, cc: mailCc.length > 0 ? mailCc : undefined,
bcc: mailBcc.length > 0 ? mailBcc : undefined, bcc: mailBcc.length > 0 ? mailBcc : undefined,
dkim_domain: process.env.NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN || undefined, dkim_domain: env('NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN') || undefined,
dkim_selector: process.env.NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR || undefined, dkim_selector: env('NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR') || undefined,
dkim_private_key: process.env.NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY || undefined, dkim_private_key: env('NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY') || undefined,
}, },
], ],
content: [ content: [

View File

@ -6,6 +6,7 @@ import {
SUPPORTED_LANGUAGE_CODES, SUPPORTED_LANGUAGE_CODES,
isValidLanguageCode, isValidLanguageCode,
} from '../../constants/i18n'; } from '../../constants/i18n';
import { env } from '../../utils/env';
import { remember } from '../../utils/remember'; import { remember } from '../../utils/remember';
type SupportedLanguages = (typeof SUPPORTED_LANGUAGE_CODES)[number]; type SupportedLanguages = (typeof SUPPORTED_LANGUAGE_CODES)[number];
@ -13,7 +14,7 @@ type SupportedLanguages = (typeof SUPPORTED_LANGUAGE_CODES)[number];
export async function loadCatalog(lang: SupportedLanguages): Promise<{ export async function loadCatalog(lang: SupportedLanguages): Promise<{
[k: string]: Messages; [k: string]: Messages;
}> { }> {
const extension = process.env.NODE_ENV === 'development' ? 'po' : 'js'; const extension = env('NODE_ENV') === 'development' ? 'po' : 'js';
// const { messages } = await import(`../../translations/${lang}/web.${extension}`); // const { messages } = await import(`../../translations/${lang}/web.${extension}`);
const messages = {}; const messages = {};

View File

@ -1,16 +1,16 @@
import { env } from 'next-runtime-env'; import { env } from '@documenso/lib/utils/env';
export const APP_DOCUMENT_UPLOAD_SIZE_LIMIT = export const APP_DOCUMENT_UPLOAD_SIZE_LIMIT =
Number(process.env.NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT) || 50; Number(env('NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT')) || 50;
// Todo: env('NEXT_PUBLIC_WEBAPP_URL') // Todo: env('NEXT_PUBLIC_WEBAPP_URL')
export const NEXT_PUBLIC_WEBAPP_URL = () => 'http://localhost:3000'; export const NEXT_PUBLIC_WEBAPP_URL = () => 'http://localhost:3000';
export const NEXT_PUBLIC_MARKETING_URL = () => env('NEXT_PUBLIC_MARKETING_URL'); export const NEXT_PUBLIC_MARKETING_URL = () => env('NEXT_PUBLIC_MARKETING_URL');
export const NEXT_PRIVATE_INTERNAL_WEBAPP_URL = export const NEXT_PRIVATE_INTERNAL_WEBAPP_URL =
process.env.NEXT_PRIVATE_INTERNAL_WEBAPP_URL ?? NEXT_PUBLIC_WEBAPP_URL(); env('NEXT_PRIVATE_INTERNAL_WEBAPP_URL') ?? NEXT_PUBLIC_WEBAPP_URL();
export const IS_APP_MARKETING = process.env.NEXT_PUBLIC_PROJECT === 'marketing'; export const IS_APP_MARKETING = env('NEXT_PUBLIC_PROJECT') === 'marketing';
export const IS_APP_WEB = process.env.NEXT_PUBLIC_PROJECT === 'web'; export const IS_APP_WEB = env('NEXT_PUBLIC_PROJECT') === 'web';
export const IS_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED') === 'true'; export const IS_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED') === 'true';
export const IS_APP_WEB_I18N_ENABLED = true; export const IS_APP_WEB_I18N_ENABLED = true;

View File

@ -1,4 +1,4 @@
// Todo: Reimport import { env } from '../utils/env';
export const SALT_ROUNDS = 12; export const SALT_ROUNDS = 12;
@ -9,17 +9,16 @@ export const IDENTITY_PROVIDER_NAME: Record<string, string> = {
}; };
export const IS_GOOGLE_SSO_ENABLED = Boolean( export const IS_GOOGLE_SSO_ENABLED = Boolean(
import.meta.env.NEXT_PRIVATE_GOOGLE_CLIENT_ID && env('NEXT_PRIVATE_GOOGLE_CLIENT_ID') && env('NEXT_PRIVATE_GOOGLE_CLIENT_SECRET'),
import.meta.env.NEXT_PRIVATE_GOOGLE_CLIENT_SECRET,
); );
export const IS_OIDC_SSO_ENABLED = Boolean( export const IS_OIDC_SSO_ENABLED = Boolean(
process.env.NEXT_PRIVATE_OIDC_WELL_KNOWN && env('NEXT_PRIVATE_OIDC_WELL_KNOWN') &&
process.env.NEXT_PRIVATE_OIDC_CLIENT_ID && env('NEXT_PRIVATE_OIDC_CLIENT_ID') &&
process.env.NEXT_PRIVATE_OIDC_CLIENT_SECRET, env('NEXT_PRIVATE_OIDC_CLIENT_SECRET'),
); );
export const OIDC_PROVIDER_LABEL = process.env.NEXT_PRIVATE_OIDC_PROVIDER_LABEL; export const OIDC_PROVIDER_LABEL = env('NEXT_PRIVATE_OIDC_PROVIDER_LABEL');
export const USER_SECURITY_AUDIT_LOG_MAP: Record<string, string> = { export const USER_SECURITY_AUDIT_LOG_MAP: Record<string, string> = {
ACCOUNT_SSO_LINK: 'Linked account to SSO', ACCOUNT_SSO_LINK: 'Linked account to SSO',
@ -49,7 +48,7 @@ export const PASSKEY_TIMEOUT = 60000;
export const MAXIMUM_PASSKEYS = 50; export const MAXIMUM_PASSKEYS = 50;
export const useSecureCookies = export const useSecureCookies =
process.env.NODE_ENV === 'production' && String(process.env.NEXTAUTH_URL).startsWith('https://'); env('NODE_ENV') === 'production' && String(env('NEXTAUTH_URL')).startsWith('https://');
const secureCookiePrefix = useSecureCookies ? '__Secure-' : ''; const secureCookiePrefix = useSecureCookies ? '__Secure-' : '';

View File

@ -1,4 +1,6 @@
export const FROM_ADDRESS = process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com'; import { env } from '../utils/env';
export const FROM_NAME = process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso';
export const FROM_ADDRESS = env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com';
export const FROM_NAME = env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso';
export const SERVICE_USER_EMAIL = 'serviceaccount@documenso.com'; export const SERVICE_USER_EMAIL = 'serviceaccount@documenso.com';

View File

@ -1,4 +1,4 @@
import { env } from 'next-runtime-env'; import { env } from '@documenso/lib/utils/env';
import { APP_BASE_URL, WEBAPP_BASE_URL } from './app'; import { APP_BASE_URL, WEBAPP_BASE_URL } from './app';

View File

@ -17,8 +17,6 @@ export enum AppErrorCode {
'RETRY_EXCEPTION' = 'RETRY_EXCEPTION', 'RETRY_EXCEPTION' = 'RETRY_EXCEPTION',
'SCHEMA_FAILED' = 'SCHEMA_FAILED', 'SCHEMA_FAILED' = 'SCHEMA_FAILED',
'TOO_MANY_REQUESTS' = 'TOO_MANY_REQUESTS', 'TOO_MANY_REQUESTS' = 'TOO_MANY_REQUESTS',
'PROFILE_URL_TAKEN' = 'PROFILE_URL_TAKEN',
'PREMIUM_PROFILE_URL' = 'PREMIUM_PROFILE_URL',
} }
export const genericErrorCodeToTrpcErrorCodeMap: Record<string, { code: string; status: number }> = export const genericErrorCodeToTrpcErrorCodeMap: Record<string, { code: string; status: number }> =
@ -34,8 +32,6 @@ export const genericErrorCodeToTrpcErrorCodeMap: Record<string, { code: string;
[AppErrorCode.RETRY_EXCEPTION]: { code: 'INTERNAL_SERVER_ERROR', status: 500 }, [AppErrorCode.RETRY_EXCEPTION]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
[AppErrorCode.SCHEMA_FAILED]: { code: 'INTERNAL_SERVER_ERROR', status: 500 }, [AppErrorCode.SCHEMA_FAILED]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
[AppErrorCode.TOO_MANY_REQUESTS]: { code: 'TOO_MANY_REQUESTS', status: 429 }, [AppErrorCode.TOO_MANY_REQUESTS]: { code: 'TOO_MANY_REQUESTS', status: 429 },
[AppErrorCode.PROFILE_URL_TAKEN]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.PREMIUM_PROFILE_URL]: { code: 'BAD_REQUEST', status: 400 },
}; };
export const ZAppErrorJsonSchema = z.object({ export const ZAppErrorJsonSchema = z.object({

View File

@ -1,5 +1,6 @@
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { env } from '../../utils/env';
import type { JobDefinition, TriggerJobOptions } from './_internal/job'; import type { JobDefinition, TriggerJobOptions } from './_internal/job';
import type { BaseJobProvider as JobClientProvider } from './base'; import type { BaseJobProvider as JobClientProvider } from './base';
import { InngestJobProvider } from './inngest'; import { InngestJobProvider } from './inngest';
@ -10,7 +11,7 @@ export class JobClient<T extends ReadonlyArray<JobDefinition> = []> {
private _provider: JobClientProvider; private _provider: JobClientProvider;
public constructor(definitions: T) { public constructor(definitions: T) {
this._provider = match(process.env.NEXT_PRIVATE_JOBS_PROVIDER) this._provider = match(env('NEXT_PRIVATE_JOBS_PROVIDER'))
.with('inngest', () => InngestJobProvider.getInstance()) .with('inngest', () => InngestJobProvider.getInstance())
.with('trigger', () => TriggerJobProvider.getInstance()) .with('trigger', () => TriggerJobProvider.getInstance())
.otherwise(() => LocalJobProvider.getInstance()); .otherwise(() => LocalJobProvider.getInstance());

View File

@ -9,6 +9,7 @@ import type { Logger } from 'inngest/middleware/logger';
import { serve as createPagesRoute } from 'inngest/next'; import { serve as createPagesRoute } from 'inngest/next';
import { json } from 'micro'; import { json } from 'micro';
import { env } from '../../utils/env';
import type { JobDefinition, JobRunIO, SimpleTriggerJobOptions } from './_internal/job'; import type { JobDefinition, JobRunIO, SimpleTriggerJobOptions } from './_internal/job';
import { BaseJobProvider } from './base'; import { BaseJobProvider } from './base';
@ -28,8 +29,8 @@ export class InngestJobProvider extends BaseJobProvider {
static getInstance() { static getInstance() {
if (!this._instance) { if (!this._instance) {
const client = new InngestClient({ const client = new InngestClient({
id: process.env.NEXT_PRIVATE_INNGEST_APP_ID || 'documenso-app', id: env('NEXT_PRIVATE_INNGEST_APP_ID') || 'documenso-app',
eventKey: process.env.INNGEST_EVENT_KEY || process.env.NEXT_PRIVATE_INNGEST_EVENT_KEY, eventKey: env('INNGEST_EVENT_KEY') || env('NEXT_PRIVATE_INNGEST_EVENT_KEY'),
}); });
this._instance = new InngestJobProvider({ client }); this._instance = new InngestJobProvider({ client });

View File

@ -2,6 +2,7 @@ import { createPagesRoute } from '@trigger.dev/nextjs';
import type { IO } from '@trigger.dev/sdk'; import type { IO } from '@trigger.dev/sdk';
import { TriggerClient, eventTrigger } from '@trigger.dev/sdk'; import { TriggerClient, eventTrigger } from '@trigger.dev/sdk';
import { env } from '../../utils/env';
import type { JobDefinition, JobRunIO, SimpleTriggerJobOptions } from './_internal/job'; import type { JobDefinition, JobRunIO, SimpleTriggerJobOptions } from './_internal/job';
import { BaseJobProvider } from './base'; import { BaseJobProvider } from './base';
@ -20,8 +21,8 @@ export class TriggerJobProvider extends BaseJobProvider {
if (!this._instance) { if (!this._instance) {
const client = new TriggerClient({ const client = new TriggerClient({
id: 'documenso-app', id: 'documenso-app',
apiKey: process.env.NEXT_PRIVATE_TRIGGER_API_KEY, apiKey: env('NEXT_PRIVATE_TRIGGER_API_KEY'),
apiUrl: process.env.NEXT_PRIVATE_TRIGGER_API_URL, apiUrl: env('NEXT_PRIVATE_TRIGGER_API_URL'),
}); });
this._instance = new TriggerJobProvider({ client }); this._instance = new TriggerJobProvider({ client });

View File

@ -10,8 +10,8 @@ import type { JWT } from 'next-auth/jwt';
import CredentialsProvider from 'next-auth/providers/credentials'; import CredentialsProvider from 'next-auth/providers/credentials';
import type { GoogleProfile } from 'next-auth/providers/google'; import type { GoogleProfile } from 'next-auth/providers/google';
import GoogleProvider from 'next-auth/providers/google'; import GoogleProvider from 'next-auth/providers/google';
import { env } from 'next-runtime-env';
import { env } from '@documenso/lib/utils/env';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { formatSecureCookieName, useSecureCookies } from '../constants/auth'; import { formatSecureCookieName, useSecureCookies } from '../constants/auth';

View File

@ -1,24 +1,28 @@
export const isErrorCode = (code: unknown): code is ErrorCode => { // export const isErrorCode = (code: unknown): code is ErrorCode => {
return typeof code === 'string' && code in ErrorCode; // return typeof code === 'string' && code in ErrorCode;
}; // };
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode]; // export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
export const ErrorCode = { // Todo: Delete file
INCORRECT_EMAIL_PASSWORD: 'INCORRECT_EMAIL_PASSWORD', // Todo: Delete file
USER_MISSING_PASSWORD: 'USER_MISSING_PASSWORD', // Todo: Delete file
CREDENTIALS_NOT_FOUND: 'CREDENTIALS_NOT_FOUND', // Todo: Delete file
INTERNAL_SEVER_ERROR: 'INTERNAL_SEVER_ERROR', // export const ErrorCode = {
TWO_FACTOR_ALREADY_ENABLED: 'TWO_FACTOR_ALREADY_ENABLED', // INCORRECT_EMAIL_PASSWORD: 'INCORRECT_EMAIL_PASSWORD',
TWO_FACTOR_SETUP_REQUIRED: 'TWO_FACTOR_SETUP_REQUIRED', // USER_MISSING_PASSWORD: 'USER_MISSING_PASSWORD',
TWO_FACTOR_MISSING_SECRET: 'TWO_FACTOR_MISSING_SECRET', // CREDENTIALS_NOT_FOUND: 'CREDENTIALS_NOT_FOUND',
TWO_FACTOR_MISSING_CREDENTIALS: 'TWO_FACTOR_MISSING_CREDENTIALS', // INTERNAL_SEVER_ERROR: 'INTERNAL_SEVER_ERROR',
INCORRECT_TWO_FACTOR_CODE: 'INCORRECT_TWO_FACTOR_CODE', // TWO_FACTOR_ALREADY_ENABLED: 'TWO_FACTOR_ALREADY_ENABLED',
INCORRECT_TWO_FACTOR_BACKUP_CODE: 'INCORRECT_TWO_FACTOR_BACKUP_CODE', // TWO_FACTOR_SETUP_REQUIRED: 'TWO_FACTOR_SETUP_REQUIRED',
INCORRECT_IDENTITY_PROVIDER: 'INCORRECT_IDENTITY_PROVIDER', // TWO_FACTOR_MISSING_SECRET: 'TWO_FACTOR_MISSING_SECRET',
INCORRECT_PASSWORD: 'INCORRECT_PASSWORD', // TWO_FACTOR_MISSING_CREDENTIALS: 'TWO_FACTOR_MISSING_CREDENTIALS',
MISSING_ENCRYPTION_KEY: 'MISSING_ENCRYPTION_KEY', // INCORRECT_TWO_FACTOR_CODE: 'INCORRECT_TWO_FACTOR_CODE',
MISSING_BACKUP_CODE: 'MISSING_BACKUP_CODE', // INCORRECT_TWO_FACTOR_BACKUP_CODE: 'INCORRECT_TWO_FACTOR_BACKUP_CODE',
UNVERIFIED_EMAIL: 'UNVERIFIED_EMAIL', // INCORRECT_IDENTITY_PROVIDER: 'INCORRECT_IDENTITY_PROVIDER',
ACCOUNT_DISABLED: 'ACCOUNT_DISABLED', // INCORRECT_PASSWORD: 'INCORRECT_PASSWORD',
} as const; // MISSING_ENCRYPTION_KEY: 'MISSING_ENCRYPTION_KEY',
// MISSING_BACKUP_CODE: 'MISSING_BACKUP_CODE',
// UNVERIFIED_EMAIL: 'UNVERIFIED_EMAIL',
// ACCOUNT_DISABLED: 'ACCOUNT_DISABLED',
// } as const;

View File

@ -3,7 +3,6 @@ import { base32 } from '@scure/base';
import crypto from 'crypto'; import crypto from 'crypto';
import { createTOTPKeyURI } from 'oslo/otp'; import { createTOTPKeyURI } from 'oslo/otp';
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto'; import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
@ -21,7 +20,7 @@ export const setupTwoFactorAuthentication = async ({
const key = DOCUMENSO_ENCRYPTION_KEY; const key = DOCUMENSO_ENCRYPTION_KEY;
if (!key) { if (!key) {
throw new Error(ErrorCode.MISSING_ENCRYPTION_KEY); throw new Error('MISSING_ENCRYPTION_KEY');
} }
const secret = crypto.randomBytes(10); const secret = crypto.randomBytes(10);

View File

@ -1,6 +1,6 @@
import type { User } from '@prisma/client'; import type { User } from '@prisma/client';
import { ErrorCode } from '../../next-auth/error-codes'; import { AppError } from '../../errors/app-error';
import { verifyTwoFactorAuthenticationToken } from './verify-2fa-token'; import { verifyTwoFactorAuthenticationToken } from './verify-2fa-token';
import { verifyBackupCode } from './verify-backup-code'; import { verifyBackupCode } from './verify-backup-code';
@ -16,11 +16,11 @@ export const validateTwoFactorAuthentication = async ({
user, user,
}: ValidateTwoFactorAuthenticationOptions) => { }: ValidateTwoFactorAuthenticationOptions) => {
if (!user.twoFactorEnabled) { if (!user.twoFactorEnabled) {
throw new Error(ErrorCode.TWO_FACTOR_SETUP_REQUIRED); throw new AppError('TWO_FACTOR_SETUP_REQUIRED');
} }
if (!user.twoFactorSecret) { if (!user.twoFactorSecret) {
throw new Error(ErrorCode.TWO_FACTOR_MISSING_SECRET); throw new AppError('TWO_FACTOR_MISSING_SECRET');
} }
if (totpCode) { if (totpCode) {
@ -31,5 +31,5 @@ export const validateTwoFactorAuthentication = async ({
return await verifyBackupCode({ user, backupCode }); return await verifyBackupCode({ user, backupCode });
} }
throw new Error(ErrorCode.TWO_FACTOR_MISSING_CREDENTIALS); throw new AppError('TWO_FACTOR_MISSING_CREDENTIALS');
}; };

View File

@ -8,6 +8,7 @@ import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n-server'; import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app'; import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n'; import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export interface SendConfirmationEmailProps { export interface SendConfirmationEmailProps {
@ -15,8 +16,8 @@ export interface SendConfirmationEmailProps {
} }
export const sendConfirmationEmail = async ({ userId }: SendConfirmationEmailProps) => { export const sendConfirmationEmail = async ({ userId }: SendConfirmationEmailProps) => {
const NEXT_PRIVATE_SMTP_FROM_NAME = process.env.NEXT_PRIVATE_SMTP_FROM_NAME; const NEXT_PRIVATE_SMTP_FROM_NAME = env('NEXT_PRIVATE_SMTP_FROM_NAME');
const NEXT_PRIVATE_SMTP_FROM_ADDRESS = process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS; const NEXT_PRIVATE_SMTP_FROM_ADDRESS = env('NEXT_PRIVATE_SMTP_FROM_ADDRESS');
const user = await prisma.user.findFirstOrThrow({ const user = await prisma.user.findFirstOrThrow({
where: { where: {

View File

@ -8,6 +8,7 @@ import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n-server'; import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app'; import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n'; import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export interface SendForgotPasswordOptions { export interface SendForgotPasswordOptions {
@ -55,8 +56,8 @@ export const sendForgotPassword = async ({ userId }: SendForgotPasswordOptions)
name: user.name || '', name: user.name || '',
}, },
from: { from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso', name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com', address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
}, },
subject: i18n._(msg`Forgot Password?`), subject: i18n._(msg`Forgot Password?`),
html, html,

View File

@ -5,6 +5,7 @@ import { ResetPasswordTemplate } from '@documenso/email/templates/reset-password
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app'; import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n'; import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export interface SendResetPasswordOptions { export interface SendResetPasswordOptions {
@ -37,8 +38,8 @@ export const sendResetPassword = async ({ userId }: SendResetPasswordOptions) =>
name: user.name || '', name: user.name || '',
}, },
from: { from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso', name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com', address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
}, },
subject: 'Password Reset Success!', subject: 'Password Reset Success!',
html, html,

View File

@ -14,6 +14,7 @@ import { extractDerivedDocumentEmailSettings } from '../../types/document-email'
import type { RequestMetadata } from '../../universal/extract-request-metadata'; import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { getFile } from '../../universal/upload/get-file'; import { getFile } from '../../universal/upload/get-file';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs'; import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { env } from '../../utils/env';
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template'; import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n'; import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding'; import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
@ -113,8 +114,8 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
}, },
], ],
from: { from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso', name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com', address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
}, },
subject: i18n._(msg`Signing Complete!`), subject: i18n._(msg`Signing Complete!`),
html, html,
@ -190,8 +191,8 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
}, },
], ],
from: { from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso', name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com', address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
}, },
subject: subject:
isDirectTemplate && document.documentMeta?.subject isDirectTemplate && document.documentMeta?.subject

View File

@ -10,6 +10,7 @@ import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app'; import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { AppError, AppErrorCode } from '../../errors/app-error'; import { AppError, AppErrorCode } from '../../errors/app-error';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email'; import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n'; import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding'; import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
@ -79,8 +80,8 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
name: name || '', name: name || '',
}, },
from: { from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso', name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com', address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
}, },
subject: i18n._(msg`Document Deleted!`), subject: i18n._(msg`Document Deleted!`),
html, html,

View File

@ -9,6 +9,7 @@ import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n-server'; import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app'; import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email'; import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n'; import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding'; import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
@ -90,8 +91,8 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
name, name,
}, },
from: { from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso', name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com', address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
}, },
subject: i18n._(msg`Waiting for others to complete signing.`), subject: i18n._(msg`Waiting for others to complete signing.`),
html, html,

View File

@ -3,6 +3,7 @@ import type { Browser } from 'playwright';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app'; import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { type SupportedLanguageCodes, isValidLanguageCode } from '../../constants/i18n'; import { type SupportedLanguageCodes, isValidLanguageCode } from '../../constants/i18n';
import { env } from '../../utils/env';
import { encryptSecondaryData } from '../crypto/encrypt'; import { encryptSecondaryData } from '../crypto/encrypt';
export type GetCertificatePdfOptions = { export type GetCertificatePdfOptions = {
@ -21,10 +22,10 @@ export const getCertificatePdf = async ({ documentId, language }: GetCertificate
let browser: Browser; let browser: Browser;
if (process.env.NEXT_PRIVATE_BROWSERLESS_URL) { if (env('NEXT_PRIVATE_BROWSERLESS_URL')) {
// !: Use CDP rather than the default `connect` method to avoid coupling to the playwright version. // !: Use CDP rather than the default `connect` method to avoid coupling to the playwright version.
// !: Previously we would have to keep the playwright version in sync with the browserless version to avoid errors. // !: Previously we would have to keep the playwright version in sync with the browserless version to avoid errors.
browser = await chromium.connectOverCDP(process.env.NEXT_PRIVATE_BROWSERLESS_URL); browser = await chromium.connectOverCDP(env('NEXT_PRIVATE_BROWSERLESS_URL'));
} else { } else {
browser = await chromium.launch(); browser = await chromium.launch();
} }

View File

@ -25,15 +25,12 @@ import {
ZRadioFieldMeta, ZRadioFieldMeta,
ZTextFieldMeta, ZTextFieldMeta,
} from '../../types/field-meta'; } from '../../types/field-meta';
import { env } from '../../utils/env';
export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignature) => { export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignature) => {
const fontCaveat = await fetch(process.env.FONT_CAVEAT_URI).then(async (res) => const fontCaveat = await fetch(env('FONT_CAVEAT_URI')).then(async (res) => res.arrayBuffer());
res.arrayBuffer(),
);
const fontNoto = await fetch(process.env.FONT_NOTO_SANS_URI).then(async (res) => const fontNoto = await fetch(env('FONT_NOTO_SANS_URI')).then(async (res) => res.arrayBuffer());
res.arrayBuffer(),
);
const isSignatureField = isSignatureFieldType(field.type); const isSignatureField = isSignatureFieldType(field.type);

View File

@ -1,8 +1,10 @@
/* eslint-disable turbo/no-undeclared-env-vars */ /* eslint-disable turbo/no-undeclared-env-vars */
import { Redis } from '@upstash/redis'; import { Redis } from '@upstash/redis';
import { env } from '../../utils/env';
// !: We're null coalescing here because we don't want local builds to fail. // !: We're null coalescing here because we don't want local builds to fail.
export const redis = new Redis({ export const redis = new Redis({
url: process.env.NEXT_PRIVATE_REDIS_URL ?? '', url: env('NEXT_PRIVATE_REDIS_URL') ?? '',
token: process.env.NEXT_PRIVATE_REDIS_TOKEN ?? '', token: env('NEXT_PRIVATE_REDIS_TOKEN') ?? '',
}); });

View File

@ -1,7 +1,9 @@
/// <reference types="./stripe.d.ts" /> /// <reference types="./stripe.d.ts" />
import Stripe from 'stripe'; import Stripe from 'stripe';
export const stripe = new Stripe(process.env.NEXT_PRIVATE_STRIPE_API_KEY ?? '', { import { env } from '../../utils/env';
export const stripe = new Stripe(env('NEXT_PRIVATE_STRIPE_API_KEY') ?? '', {
apiVersion: '2022-11-15', apiVersion: '2022-11-15',
typescript: true, typescript: true,
}); });

View File

@ -15,6 +15,7 @@ import { createTokenVerification } from '@documenso/lib/utils/token-verification
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n-server'; import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n'; import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding'; import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
@ -122,7 +123,7 @@ export const sendTeamEmailVerificationEmail = async (
teamGlobalSettings?: TeamGlobalSettings | null; teamGlobalSettings?: TeamGlobalSettings | null;
}, },
) => { ) => {
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000'; const assetBaseUrl = env('NEXT_PUBLIC_WEBAPP_URL') || 'http://localhost:3000';
const template = createElement(ConfirmTeamEmailTemplate, { const template = createElement(ConfirmTeamEmailTemplate, {
assetBaseUrl, assetBaseUrl,

Some files were not shown because too many files have changed in this diff Show More