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');
}
// Todo: Test
void navigate(`/documents?${params.toString()}`);
void navigate(`?${params.toString()}`);
},
[searchParams],
);

View File

@ -20,7 +20,7 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { Skeleton } from '@documenso/ui/primitives/skeleton';
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 = () => {
const { _, i18n } = useLingui();
@ -99,7 +99,7 @@ export const CurrentUserTeamsDataTable = () => {
</Button>
)}
<LeaveTeamDialog
<TeamLeaveDialog
teamId={row.original.id}
teamName={row.original.name}
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 { 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';
export const PendingUserTeamsDataTable = () => {
@ -139,7 +140,7 @@ export const PendingUserTeamsDataTable = () => {
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
</DataTable>
<CreateTeamCheckoutDialog
<TeamCheckoutCreateDialog
pendingTeamId={checkoutPendingTeamId}
onClose={() => setCheckoutPendingTeamId(null)}
/>

View File

@ -26,8 +26,8 @@ import {
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { DeleteTeamMemberDialog } from '../dialogs/delete-team-member-dialog';
import { UpdateTeamMemberDialog } from '../dialogs/update-team-member-dialog';
import { DeleteTeamMemberDialog } from '../../dialogs/team-member-delete-dialog';
import { UpdateTeamMemberDialog } from '../../dialogs/team-member-update-dialog';
export type TeamMembersDataTableProps = {
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 { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team';
type DocumentDuplicateDialogProps = {
id: number;
open: boolean;
onOpenChange: (_open: boolean) => void;
team?: Pick<Team, 'id' | 'url'>;
};
export const DocumentDuplicateDialog = ({
id,
open,
onOpenChange,
team,
}: DocumentDuplicateDialogProps) => {
const navigate = useNavigate();
const { toast } = useToast();
const { _ } = useLingui();
const team = useOptionalCurrentTeam();
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery({
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 { useAuth } from '~/providers/auth';
import { useOptionalCurrentTeam } from '~/providers/team';
const FORM_ID = 'resend-email';
@ -44,7 +45,6 @@ export type DocumentResendDialogProps = {
team: Pick<Team, 'id' | 'url'> | null;
};
recipients: Recipient[];
team?: Pick<Team, 'id' | 'url'>;
};
export const ZResendDocumentFormSchema = z.object({
@ -55,8 +55,9 @@ export const ZResendDocumentFormSchema = z.object({
export type TResendDocumentFormSchema = z.infer<typeof ZResendDocumentFormSchema>;
export const DocumentResendDialog = ({ document, recipients, team }: DocumentResendDialogProps) => {
export const DocumentResendDialog = ({ document, recipients }: DocumentResendDialogProps) => {
const { user } = useAuth();
const team = useOptionalCurrentTeam();
const { toast } = useToast();
const { _ } = useLingui();

View File

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

View File

@ -35,7 +35,7 @@ import {
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type CreateTeamDialogProps = {
export type TeamCreateDialogProps = {
trigger?: React.ReactNode;
} & Omit<DialogPrimitive.DialogProps, 'children'>;
@ -46,7 +46,7 @@ const ZCreateTeamFormSchema = ZCreateTeamMutationSchema.pick({
type TCreateTeamFormSchema = z.infer<typeof ZCreateTeamFormSchema>;
export const CreateTeamDialog = ({ trigger, ...props }: CreateTeamDialogProps) => {
export const TeamCreateDialog = ({ trigger, ...props }: TeamCreateDialogProps) => {
const { _ } = useLingui();
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 { useToast } from '@documenso/ui/primitives/use-toast';
export type DeleteTeamDialogProps = {
export type TeamDeleteDialogProps = {
teamId: number;
teamName: string;
trigger?: React.ReactNode;
};
export const DeleteTeamDialog = ({ trigger, teamId, teamName }: DeleteTeamDialogProps) => {
export const TeamDeleteDialog = ({ trigger, teamId, teamName }: TeamDeleteDialogProps) => {
const navigate = useNavigate();
const [open, setOpen] = useState(false);

View File

@ -32,7 +32,7 @@ import {
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type AddTeamEmailDialogProps = {
export type TeamEmailAddDialogProps = {
teamId: number;
trigger?: React.ReactNode;
} & Omit<DialogPrimitive.DialogProps, 'children'>;
@ -44,7 +44,7 @@ const ZCreateTeamEmailFormSchema = ZCreateTeamEmailVerificationMutationSchema.pi
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 { _ } = useLingui();

View File

@ -21,7 +21,7 @@ import {
} from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type RemoveTeamEmailDialogProps = {
export type TeamEmailDeleteDialogProps = {
trigger?: React.ReactNode;
teamName: string;
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 { _ } = useLingui();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -88,12 +88,12 @@ export const ClaimPublicProfileDialogForm = ({
} catch (err) {
const error = AppError.parseError(err);
if (error.code === AppErrorCode.PROFILE_URL_TAKEN) {
if (error.code === 'PROFILE_URL_TAKEN') {
form.setError('url', {
type: 'manual',
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', {
type: 'manual',
message: error.message,

View File

@ -11,7 +11,7 @@ import { useForm } from 'react-hook-form';
import type { z } from 'zod';
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 {
MAX_PROFILE_BIO_LENGTH,
@ -88,8 +88,8 @@ export const PublicProfileForm = ({
const error = AppError.parseError(err);
switch (error.code) {
case AppErrorCode.PREMIUM_PROFILE_URL:
case AppErrorCode.PROFILE_URL_TAKEN:
case 'PREMIUM_PROFILE_URL':
case 'PROFILE_URL_TAKEN':
form.setError('url', {
type: 'manual',
message: error.message,

View File

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

View File

@ -12,9 +12,9 @@ import { Link, useNavigate, useSearchParams } from 'react-router';
import { z } from 'zod';
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 { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
@ -71,8 +71,8 @@ export const signupErrorMessages: Record<string, MessageDescriptor> = {
SIGNUP_DISABLED: msg`Signups are disabled.`,
[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.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`,
PROFILE_URL_TAKEN: msg`This username has already been taken`,
PREMIUM_PROFILE_URL: msg`Only subscribers can have a username shorter than 6 characters`,
};
export type TSignUpFormSchema = z.infer<typeof ZSignUpFormSchema>;
@ -93,7 +93,7 @@ export const SignUpForm = ({
const { _ } = useLingui();
const { toast } = useToast();
// const analytics = useAnalytics();
// const analytics = useAnalytics(); // Todo
const navigate = useNavigate();
const [searchParams] = useSearchParams();
@ -120,11 +120,9 @@ export const SignUpForm = ({
const name = form.watch('name');
const url = form.watch('url');
const { mutateAsync: signup } = trpc.auth.signup.useMutation();
const onFormSubmit = async ({ name, email, password, signature, url }: TSignUpFormSchema) => {
try {
await signup({ name, email, password, signature, url });
await authClient.emailPassword.signUp({ name, email, password, signature, url });
void navigate(`/unverified-account`);
@ -146,10 +144,7 @@ export const SignUpForm = ({
const errorMessage = signupErrorMessages[error.code] ?? signupErrorMessages.INVALID_REQUEST;
if (
error.code === AppErrorCode.PROFILE_URL_TAKEN ||
error.code === AppErrorCode.PREMIUM_PROFILE_URL
) {
if (error.code === 'PROFILE_URL_TAKEN' || error.code === 'PREMIUM_PROFILE_URL') {
form.setError('url', {
type: 'manual',
message: _(errorMessage),
@ -175,8 +170,7 @@ export const SignUpForm = ({
const onSignUpWithGoogleClick = async () => {
try {
await new Promise((resolve) => setTimeout(resolve, 2000));
// await signIn('google', { callbackUrl: SIGN_UP_REDIRECT_PATH });
await authClient.google.signIn();
} catch (err) {
toast({
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 { useToast } from '@documenso/ui/primitives/use-toast';
export type DownloadAuditLogButtonProps = {
export type DocumentAuditLogDownloadButtonProps = {
className?: string;
teamId?: number;
documentId: number;
};
export const DownloadAuditLogButton = ({ className, documentId }: DownloadAuditLogButtonProps) => {
export const DocumentAuditLogDownloadButton = ({
className,
documentId,
}: DocumentAuditLogDownloadButtonProps) => {
const { toast } = useToast();
const { _ } = useLingui();

View File

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

View File

@ -3,6 +3,7 @@ import { useEffect, useState } from 'react';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { DocumentDistributionMethod, DocumentStatus } from '@prisma/client';
import { useNavigate, useSearchParams } from 'react-router';
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
import {
@ -29,7 +30,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team';
export type EditDocumentFormProps = {
export type DocumentEditFormProps = {
className?: string;
initialDocument: TDocument;
documentRootPath: string;
@ -39,17 +40,18 @@ export type EditDocumentFormProps = {
type EditDocumentStep = 'settings' | 'signers' | 'fields' | 'subject';
const EditDocumentSteps: EditDocumentStep[] = ['settings', 'signers', 'fields', 'subject'];
export const EditDocumentForm = ({
export const DocumentEditForm = ({
className,
initialDocument,
documentRootPath,
isDocumentEnterprise,
}: EditDocumentFormProps) => {
}: DocumentEditFormProps) => {
const { toast } = useToast();
const { _ } = useLingui();
const router = useRouter();
const searchParams = useSearchParams();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const team = useOptionalCurrentTeam();
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');
} catch (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');
} catch (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');
} catch (err) {
console.error(err);
@ -301,7 +294,7 @@ export const EditDocumentForm = ({
duration: 5000,
});
router.push(documentRootPath);
void navigate(documentRootPath);
return;
}
@ -312,7 +305,7 @@ export const EditDocumentForm = ({
duration: 5000,
});
} else {
router.push(`${documentRootPath}/${document.id}`);
void navigate(`${documentRootPath}/${document.id}`);
}
} catch (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 { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
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 { 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 { useToast } from '@documenso/ui/primitives/use-toast';
import { useAuth } from '~/providers/auth';
export type DocumentPageViewButtonProps = {
document: Document & {
user: Pick<User, 'id' | 'name' | 'email'>;
recipients: Recipient[];
team: Pick<Team, 'id' | 'url'> | null;
};
team?: Pick<Team, 'id' | 'url'>;
};
export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps) => {
const { data: session } = useSession();
const { user } = useAuth();
const { toast } = useToast();
const { _ } = useLingui();
if (!session) {
return null;
}
const recipient = document.recipients.find((recipient) => recipient.email === session.user.email);
const recipient = document.recipients.find((recipient) => recipient.email === user.email);
const isRecipient = !!recipient;
const isPending = document.status === DocumentStatus.PENDING;
@ -77,7 +75,7 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
})
.with({ isRecipient: true, isPending: true, isSigned: false }, () => (
<Button className="w-full" asChild>
<Link href={`/sign/${recipient?.token}`}>
<Link to={`/sign/${recipient?.token}`}>
{match(role)
.with(RecipientRole.SIGNER, () => (
<>
@ -102,7 +100,7 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
))
.with({ isComplete: false }, () => (
<Button className="w-full" asChild>
<Link href={`${documentsPath}/${document.id}/edit`}>
<Link to={`${documentsPath}/${document.id}/edit`}>
<Trans>Edit</Trans>
</Link>
</Button>

View File

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

View File

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

View File

@ -1,5 +1,9 @@
'use client';
import { useMemo } from 'react';
import { useSearchParams } from 'next/navigation';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
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 { TableCell } from '@documenso/ui/primitives/table';
export type DocumentLogsDataTableProps = {
export type DocumentLogsTableProps = {
documentId: number;
};
@ -25,7 +29,7 @@ const dateFormat: DateTimeFormatOptions = {
hourCycle: 'h12',
};
export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps) => {
export const DocumentLogsTable = ({ documentId }: DocumentLogsTableProps) => {
const { _, i18n } = useLingui();
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,
ScrollRestoration,
isRouteErrorResponse,
useLoaderData,
} from 'react-router';
import { TrpcProvider } from '@documenso/trpc/react';
@ -32,7 +33,17 @@ export const links: Route.LinksFunction = () => [
{ 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 }) {
const { __ENV__ } = useLoaderData<typeof loader>() || {};
return (
<html lang="en">
<head>
@ -45,6 +56,12 @@ export function Layout({ children }: { children: React.ReactNode }) {
{children}
<ScrollRestoration />
<Scripts />
<script
dangerouslySetInnerHTML={{
__html: `window.__ENV__ = ${JSON.stringify(__ENV__)}`,
}}
/>
</body>
</html>
);

View File

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

View File

@ -1,20 +1,20 @@
import { Plural, Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { DocumentStatus } from '@prisma/client';
import type { Team, TeamEmail } from '@prisma/client';
import { TeamMemberRole } from '@prisma/client';
import { ChevronLeft, Clock9, Users2 } from 'lucide-react';
import { Link, redirect } from 'react-router';
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 { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
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 { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -28,24 +28,38 @@ import {
DocumentStatus as DocumentStatusComponent,
FRIENDLY_STATUS_MAP,
} 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 { 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';
import type { Route } from './+types/$id._index';
export type DocumentPageViewProps = {
documentId: number;
team?: Team & { teamEmail: TeamEmail | null } & { currentTeamMember: { role: TeamMemberRole } };
};
export async function loader({ request, params }: Route.LoaderArgs) {
const { id } = params;
export const DocumentPageView = async ({ documentId, team }: DocumentPageViewProps) => {
const { _ } = useLingui();
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);
const documentRootPath = formatDocumentsPath(team?.url);
const { user } = await getRequiredServerComponentSession();
if (!documentId || Number.isNaN(documentId)) {
return redirect(documentRootPath);
}
const document = await getDocumentById({
documentId,
@ -54,7 +68,7 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
}).catch(() => null);
if (document?.teamId && !team?.url) {
redirect(documentRootPath);
return redirect(documentRootPath);
}
const documentVisibility = document?.visibility;
@ -73,19 +87,15 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
.otherwise(() => false);
}
const isDocumentHistoryEnabled = await getServerComponentFlag(
'app_document_page_view_history_sheet',
);
if (!document || !document.documentData || (team && !canAccessDocument)) {
redirect(documentRootPath);
return redirect(documentRootPath);
}
if (team && !canAccessDocument) {
redirect(documentRootPath);
return redirect(documentRootPath);
}
const { documentData, documentMeta } = document;
const { documentMeta } = document;
if (documentMeta?.password) {
const key = DOCUMENSO_ENCRYPTION_KEY;
@ -104,6 +114,7 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
documentMeta.password = securePassword;
}
// Todo: Get full document instead???
const [recipients, fields] = await Promise.all([
getRecipientsForDocument({
documentId,
@ -122,13 +133,30 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
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 (
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
{document.status === DocumentStatus.PENDING && (
<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" />
<Trans>Documents</Trans>
</Link>
@ -207,7 +235,7 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
{_(FRIENDLY_STATUS_MAP[document.status].labelExtended)}
</h3>
<DocumentPageViewDropdown document={documentWithRecipients} team={team} />
<DocumentPageViewDropdown document={document} />
</div>
<p className="text-muted-foreground mt-2 px-4 text-sm">
@ -235,18 +263,15 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
</p>
<div className="mt-4 border-t px-4 pt-4">
<DocumentPageViewButton document={documentWithRecipients} team={team} />
<DocumentPageViewButton document={document} />
</div>
</section>
{/* Document information section. */}
<DocumentPageViewInformation document={documentWithRecipients} userId={user.id} />
<DocumentPageViewInformation document={document} userId={user.id} />
{/* Recipients section. */}
<DocumentPageViewRecipients
document={documentWithRecipients}
documentRootPath={documentRootPath}
/>
<DocumentPageViewRecipients document={document} documentRootPath={documentRootPath} />
{/* Recent activity section. */}
<DocumentPageViewRecentActivity documentId={document.id} userId={user.id} />
@ -255,4 +280,4 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
</div>
</div>
);
};
}

View File

@ -1,37 +1,49 @@
import { Plural, Trans } from '@lingui/macro';
import type { Team } from '@prisma/client';
import { TeamMemberRole } from '@prisma/client';
import { DocumentStatus as InternalDocumentStatus } from '@prisma/client';
import { ChevronLeft, Users2 } from 'lucide-react';
import { Link, redirect } from 'react-router';
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 { 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 { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
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 = {
documentId: number;
team?: Team & { currentTeamMember: { role: TeamMemberRole } };
};
export async function loader({ request, params }: Route.LoaderArgs) {
const { id } = params;
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);
if (!documentId || Number.isNaN(documentId)) {
redirect(documentRootPath);
return redirect(documentRootPath);
}
const { user } = await getRequiredServerComponentSession();
const document = await getDocumentWithDetailsById({
documentId,
userId: user.id,
@ -39,7 +51,7 @@ export const DocumentEditPageView = async ({ documentId, team }: DocumentEditPag
}).catch(() => null);
if (document?.teamId && !team?.url) {
redirect(documentRootPath);
return redirect(documentRootPath);
}
const documentVisibility = document?.visibility;
@ -59,15 +71,15 @@ export const DocumentEditPageView = async ({ documentId, team }: DocumentEditPag
}
if (!document) {
redirect(documentRootPath);
return redirect(documentRootPath);
}
if (team && !canAccessDocument) {
redirect(documentRootPath);
return redirect(documentRootPath);
}
if (document.status === InternalDocumentStatus.COMPLETED) {
redirect(`${documentRootPath}/${documentId}`);
return redirect(`${documentRootPath}/${documentId}`);
}
const { documentMeta, recipients } = document;
@ -94,9 +106,21 @@ export const DocumentEditPageView = async ({ documentId, team }: DocumentEditPag
teamId: team?.id,
});
return {
document,
documentRootPath,
isDocumentEnterprise,
};
}
export default function DocumentEditPage({ loaderData }: Route.ComponentProps) {
const { document, documentRootPath, isDocumentEnterprise } = loaderData;
const { recipients } = document;
return (
<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" />
<Trans>Documents</Trans>
</Link>
@ -128,7 +152,7 @@ export const DocumentEditPageView = async ({ documentId, team }: DocumentEditPag
)}
</div>
<EditDocumentForm
<DocumentEditForm
className="mt-6"
initialDocument={document}
documentRootPath={documentRootPath}
@ -136,4 +160,4 @@ export const DocumentEditPageView = async ({ documentId, team }: DocumentEditPag
/>
</div>
);
};
}

View File

@ -4,36 +4,50 @@ import { useLingui } from '@lingui/react';
import type { Recipient } from '@prisma/client';
import { ChevronLeft } from 'lucide-react';
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 { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { Card } from '@documenso/ui/primitives/card';
import {
DocumentStatus as DocumentStatusComponent,
FRIENDLY_STATUS_MAP,
} 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 { DownloadAuditLogButton } from './download-audit-log-button';
import { DownloadCertificateButton } from './download-certificate-button';
import type { Route } from './+types/$id.logs';
export type DocumentLogsPageViewProps = {
documentId: number;
};
export async function loader({ request, params }: Route.LoaderArgs) {
const { id } = params;
export const DocumentLogsPageView = async ({ documentId }: DocumentLogsPageViewProps) => {
const { _, i18n } = useLingui();
const { user } = await getRequiredSession(request);
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 { user } = await getRequiredServerComponentSession();
if (!documentId || Number.isNaN(documentId)) {
return redirect(documentRootPath);
}
// Todo: Get detailed?
const [document, recipients] = await Promise.all([
getDocumentById({
documentId,
@ -48,9 +62,21 @@ export const DocumentLogsPageView = async ({ documentId }: DocumentLogsPageViewP
]);
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 }[] = [
{
description: msg`Document title`,
@ -97,11 +123,10 @@ export const DocumentLogsPageView = async ({ documentId }: DocumentLogsPageViewP
return `[${recipient.role}] ${text}`;
};
return (
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
<Link
href={`${documentRootPath}/${document.id}`}
to={`${documentRootPath}/${document.id}`}
className="flex items-center text-[#7AC455] hover:opacity-80"
>
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
@ -127,14 +152,13 @@ export const DocumentLogsPageView = async ({ documentId }: DocumentLogsPageViewP
</div>
<div className="mt-4 flex w-full flex-row sm:mt-0 sm:w-auto sm:self-end">
<DownloadCertificateButton
<DocumentCertificateDownloadButton
className="mr-2"
documentId={document.id}
documentStatus={document.status}
teamId={team?.id}
/>
<DownloadAuditLogButton teamId={team?.id} documentId={document.id} />
<DocumentAuditLogDownloadButton documentId={document.id} />
</div>
</div>
@ -161,8 +185,8 @@ export const DocumentLogsPageView = async ({ documentId }: DocumentLogsPageViewP
</section>
<section className="mt-6">
<DocumentLogsDataTable documentId={document.id} />
<DocumentLogsTable documentId={document.id} />
</section>
</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 { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
import { AccountDeleteDialog } from '~/components/dialogs/account-delete-dialog';
import { AvatarImageForm } from '~/components/forms/avatar-image';
import { ProfileForm } from '~/components/forms/profile';
import type { Route } from './+types/profile';
// import { DeleteAccountDialog } from './settings/profile/delete-account-dialog';
export function meta(_args: Route.MetaArgs) {
export function meta() {
return [{ title: 'Profile' }];
}
@ -28,7 +25,7 @@ export default function SettingsProfile() {
<hr className="my-4 max-w-xl" />
{/* <DeleteAccountDialog className="max-w-xl" /> */}
<AccountDeleteDialog className="max-w-xl" />
</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 { 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 { TeamCreateDialog } from '~/components/dialogs/team-create-dialog';
import { TeamEmailUsage } from './team-email-usage';
import { TeamInvitations } from './team-invitations';
@ -23,7 +23,7 @@ export default function TeamsSettingsPage() {
title={_(msg`Teams`)}
subtitle={_(msg`Manage all teams you are currently associated with.`)}
>
<CreateTeamDialog />
<TeamCreateDialog />
</SettingsHeader>
<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 { Link, redirect } from 'react-router';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import {
IS_GOOGLE_SSO_ENABLED,
IS_OIDC_SSO_ENABLED,
OIDC_PROVIDER_LABEL,
} from '@documenso/lib/constants/auth';
import { env } from '@documenso/lib/utils/env';
import { SignInForm } from '~/components/forms/signin';
import type { Route } from './+types/signin';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
export function meta(_args: Route.MetaArgs) {
return [{ title: 'Sign In' }];
}
export async function loader({ request }: Route.LoaderArgs) {
const session = await getSession(request)
const session = await getSession(request);
if (session.isAuthenticated) {
return redirect('/documents');
@ -25,9 +26,7 @@ export async function loader({ request }: Route.LoaderArgs) {
}
export default function SignIn() {
// Todo
// const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
const NEXT_PUBLIC_DISABLE_SIGNUP = 'false';
const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
return (
<div className="w-screen max-w-lg px-4">

View File

@ -1,19 +1,16 @@
import { redirect } from 'react-router';
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 type { Route } from './+types/_unauth.signup';
export function meta(_args: Route.MetaArgs) {
export function meta() {
return [{ title: 'Sign Up' }];
}
export function loader() {
// Todo
// const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
const NEXT_PUBLIC_DISABLE_SIGNUP: string = 'false';
const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
if (NEXT_PUBLIC_DISABLE_SIGNUP === 'true') {
return redirect('/signin');

View File

@ -7,7 +7,8 @@
"dev": "react-router dev",
"start": "cross-env NODE_ENV=production node dist/server/index.js",
"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": {
"@epic-web/remember": "^1.1.0",

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 path from 'path';
import tailwindcss from 'tailwindcss';
import { defineConfig } from 'vite';
import { defineConfig, loadEnv } from 'vite';
import macrosPlugin from 'vite-plugin-babel-macros';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
envDir: path.join(__dirname, '../../'),
envPrefix: 'NEXT_',
envPrefix: '__DO_NOT_USE_OR_YOU_WILL_BE_FIRED__',
define: {
'process.env': {},
'process.env': {
...process.env,
...loadEnv('development', path.join(__dirname, '../../'), ''),
},
},
css: {
postcss: {
@ -20,6 +23,7 @@ export default defineConfig({
},
},
ssr: {
// , 'next/font/google' doesnot work
noExternal: ['react-dropzone', 'recharts'],
},
server: {
@ -36,7 +40,7 @@ export default defineConfig({
tsconfigPaths(),
],
// 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,
// },
});

View File

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

View File

@ -76,10 +76,10 @@ export const ZSignUpFormV2Schema = z
export const signupErrorMessages: Record<string, MessageDescriptor> = {
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.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>;

View File

@ -7,8 +7,8 @@ import type { AuthAppType } from '../server';
import type {
TForgotPasswordSchema,
TResetPasswordSchema,
TSignInFormSchema,
TSignUpRequestSchema,
TSignInSchema,
TSignUpSchema,
TVerifyEmailSchema,
} from '../server/types/email-password';
import type { TPasskeyAuthorizeSchema } from '../server/types/passkey';
@ -32,18 +32,6 @@ export class AuthClient {
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>) {
if (!response.ok) {
const error = await response.json();
@ -59,9 +47,16 @@ export class AuthClient {
}
public emailPassword = {
signIn: async (data: TSignInFormSchema) => {
const response = await this.client['email-password'].authorize.$post({ json: data });
return this.handleResponse(response);
signIn: async (data: TSignInSchema & { redirectUrl?: string }) => {
const response = await this.client['email-password'].authorize
.$post({ json: data })
.then(this.handleResponse);
if (data.redirectUrl) {
window.location.href = data.redirectUrl;
}
return response;
},
forgotPassword: async (data: TForgotPasswordSchema) => {
@ -74,7 +69,7 @@ export class AuthClient {
return this.handleResponse(response);
},
signUp: async (data: TSignUpRequestSchema) => {
signUp: async (data: TSignUpSchema) => {
const response = await this.client['email-password']['signup'].$post({ json: data });
return this.handleResponse(response);
},
@ -85,20 +80,29 @@ export class AuthClient {
},
};
public google = {
signIn: async () => {
const response = await this.client['google'].authorize.$post();
public passkey = {
signIn: async (data: TPasskeyAuthorizeSchema & { redirectUrl?: string }) => {
const response = await this.client['passkey'].authorize
.$post({ json: data })
.then(this.handleResponse);
// const parsedResponse = this.handleResponse(response);
if (!response.ok) {
const error = await response.json();
throw AppError.parseError(error);
if (data.redirectUrl) {
window.location.href = data.redirectUrl;
}
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) => {
if (process.env.NODE_ENV === 'development') {
if (env('NODE_ENV') === 'development') {
console.log(`[DEBUG]: ${message}`);
}
};

View File

@ -4,6 +4,7 @@ import { Hono } from 'hono';
import { DateTime } from 'luxon';
import { z } from 'zod';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { AppError } from '@documenso/lib/errors/app-error';
import { jobsClient } from '@documenso/lib/jobs/client';
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 { 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 { 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 { UserSecurityAuditLogType } from '@documenso/prisma/client';
import { AuthenticationErrorCode } from '../lib/errors/error-codes';
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 {
ZForgotPasswordSchema,
ZResetPasswordSchema,
ZSignInFormSchema,
ZSignUpRequestSchema,
ZSignInSchema,
ZSignUpSchema,
ZUpdatePasswordSchema,
ZVerifyEmailSchema,
} from '../types/email-password';
@ -36,7 +43,7 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
/**
* Authorize endpoint.
*/
.post('/authorize', zValidator('json', ZSignInFormSchema), async (c) => {
.post('/authorize', zValidator('json', ZSignInSchema), async (c) => {
const requestMetadata = c.get('requestMetadata');
const { email, password, totpCode, backupCode } = c.req.valid('json');
@ -48,8 +55,8 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
});
if (!user || !user.password) {
throw new AppError(AuthenticationErrorCode.NotFound, {
message: 'User not found',
throw new AppError(AuthenticationErrorCode.InvalidCredentials, {
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.
*/
.post('/signup', zValidator('json', ZSignUpRequestSchema), async (c) => {
// if (NEXT_PUBLIC_DISABLE_SIGNUP() === 'true') {
// throw new AppError('SIGNUP_DISABLED', {
// message: 'Signups are disabled.',
// });
// }
.post('/signup', zValidator('json', ZSignUpSchema), async (c) => {
if (env('NEXT_PUBLIC_DISABLE_SIGNUP') === 'true') {
throw new AppError('SIGNUP_DISABLED', {
message: 'Signups are disabled.',
});
}
const { name, email, password, signature, url } = c.req.valid('json');
// if (IS_BILLING_ENABLED() && url && url.length < 6) {
// throw new AppError(AppErrorCode.PREMIUM_PROFILE_URL, {
// message: 'Only subscribers can have a username shorter than 6 characters',
// });
// }
if (IS_BILLING_ENABLED() && url && url.length < 6) {
throw new AppError('PREMIUM_PROFILE_URL', {
message: 'Only subscribers can have a username shorter than 6 characters',
});
}
const user = await createUser({ name, email, password, signature, url });
@ -149,18 +156,59 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
},
});
// Todo: Check this.
return c.json({
user,
return c.text('OK', 201);
})
/**
* 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.
*/
.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.
@ -180,9 +228,12 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
.post('/reset-password', zValidator('json', ZResetPasswordSchema), async (c) => {
const { token, password } = c.req.valid('json');
const requestMetadata = c.get('requestMetadata');
await resetPassword({
token,
password,
requestMetadata,
});
return c.text('OK', 201);
@ -291,9 +342,7 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
requestMetadata,
});
return c.json({
success: true,
});
return c.text('OK', 201);
},
)
/**

View File

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

View File

@ -5,14 +5,14 @@ export const ZCurrentPasswordSchema = z
.min(6, { message: 'Must be at least 6 characters in length' })
.max(72);
export const ZSignInFormSchema = z.object({
export const ZSignInSchema = z.object({
email: z.string().email().min(1),
password: ZCurrentPasswordSchema,
totpCode: 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
.string()
@ -31,7 +31,7 @@ export const ZPasswordSchema = z
message: 'One special character is required',
});
export const ZSignUpRequestSchema = z.object({
export const ZSignUpSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
password: ZPasswordSchema,
@ -47,7 +47,7 @@ export const ZSignUpRequestSchema = z.object({
.optional(),
});
export type TSignUpRequestSchema = z.infer<typeof ZSignUpRequestSchema>;
export type TSignUpSchema = z.infer<typeof ZSignUpSchema>;
export const ZForgotPasswordSchema = z.object({
email: z.string().email().min(1),
@ -67,3 +67,10 @@ export const ZVerifyEmailSchema = z.object({
});
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 { createTeamFromPendingTeam } from '@documenso/lib/server-only/team/create-team';
import { getFlag } from '@documenso/lib/universal/get-feature-flag';
import { env } from '@documenso/lib/utils/env';
import { prisma } from '@documenso/prisma';
import { onSubscriptionDeleted } from './on-subscription-deleted';
@ -47,7 +48,7 @@ export const stripeWebhookHandler = async (
const event = stripe.webhooks.constructEvent(
body,
signature,
process.env.NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET,
env('NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET'), // Todo: Test
);
await match(event.type)

View File

@ -1,6 +1,7 @@
import type { Transporter } from 'nodemailer';
import { createTransport } from 'nodemailer';
import { env } from '@documenso/lib/utils/env';
import { ResendTransport } from '@documenso/nodemailer-resend';
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.
*/
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') {
return createTransport(
MailChannelsTransport.makeTransport({
apiKey: process.env.NEXT_PRIVATE_MAILCHANNELS_API_KEY,
endpoint: process.env.NEXT_PRIVATE_MAILCHANNELS_ENDPOINT,
apiKey: env('NEXT_PRIVATE_MAILCHANNELS_API_KEY'),
endpoint: env('NEXT_PRIVATE_MAILCHANNELS_ENDPOINT'),
}),
);
}
@ -65,43 +66,41 @@ const getTransport = (): Transporter => {
if (transport === 'resend') {
return createTransport(
ResendTransport.makeTransport({
apiKey: process.env.NEXT_PRIVATE_RESEND_API_KEY || '',
apiKey: env('NEXT_PRIVATE_RESEND_API_KEY') || '',
}),
);
}
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(
'SMTP API transport requires NEXT_PRIVATE_SMTP_HOST and NEXT_PRIVATE_SMTP_APIKEY',
);
}
return createTransport({
host: process.env.NEXT_PRIVATE_SMTP_HOST,
port: Number(process.env.NEXT_PRIVATE_SMTP_PORT) || 587,
secure: process.env.NEXT_PRIVATE_SMTP_SECURE === 'true',
host: env('NEXT_PRIVATE_SMTP_HOST'),
port: Number(env('NEXT_PRIVATE_SMTP_PORT')) || 587,
secure: env('NEXT_PRIVATE_SMTP_SECURE') === 'true',
auth: {
user: process.env.NEXT_PRIVATE_SMTP_APIKEY_USER ?? 'apikey',
pass: process.env.NEXT_PRIVATE_SMTP_APIKEY ?? '',
user: env('NEXT_PRIVATE_SMTP_APIKEY_USER') ?? 'apikey',
pass: env('NEXT_PRIVATE_SMTP_APIKEY') ?? '',
},
});
}
return createTransport({
host: process.env.NEXT_PRIVATE_SMTP_HOST ?? '127.0.0.1:2500',
port: Number(process.env.NEXT_PRIVATE_SMTP_PORT) || 587,
secure: process.env.NEXT_PRIVATE_SMTP_SECURE === 'true',
ignoreTLS: process.env.NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS === 'true',
auth: process.env.NEXT_PRIVATE_SMTP_USERNAME
host: env('NEXT_PRIVATE_SMTP_HOST') ?? '127.0.0.1:2500',
port: Number(env('NEXT_PRIVATE_SMTP_PORT')) || 587,
secure: env('NEXT_PRIVATE_SMTP_SECURE') === 'true',
ignoreTLS: env('NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS') === 'true',
auth: env('NEXT_PRIVATE_SMTP_USERNAME')
? {
user: process.env.NEXT_PRIVATE_SMTP_USERNAME,
pass: process.env.NEXT_PRIVATE_SMTP_PASSWORD ?? '',
user: env('NEXT_PRIVATE_SMTP_USERNAME'),
pass: env('NEXT_PRIVATE_SMTP_PASSWORD') ?? '',
}
: undefined,
...(process.env.NEXT_PRIVATE_SMTP_SERVICE
? { service: process.env.NEXT_PRIVATE_SMTP_SERVICE }
: {}),
...(env('NEXT_PRIVATE_SMTP_SERVICE') ? { service: env('NEXT_PRIVATE_SMTP_SERVICE') } : {}),
});
};

View File

@ -1,5 +1,6 @@
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 { TemplateDocumentImage } from './template-document-image';

View File

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

View File

@ -6,6 +6,7 @@ import {
SUPPORTED_LANGUAGE_CODES,
isValidLanguageCode,
} from '../../constants/i18n';
import { env } from '../../utils/env';
import { remember } from '../../utils/remember';
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<{
[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 = {};

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 =
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')
export const NEXT_PUBLIC_WEBAPP_URL = () => 'http://localhost:3000';
export const NEXT_PUBLIC_MARKETING_URL = () => env('NEXT_PUBLIC_MARKETING_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_WEB = process.env.NEXT_PUBLIC_PROJECT === 'web';
export const IS_APP_MARKETING = env('NEXT_PUBLIC_PROJECT') === 'marketing';
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_APP_WEB_I18N_ENABLED = true;

View File

@ -1,4 +1,4 @@
// Todo: Reimport
import { env } from '../utils/env';
export const SALT_ROUNDS = 12;
@ -9,17 +9,16 @@ export const IDENTITY_PROVIDER_NAME: Record<string, string> = {
};
export const IS_GOOGLE_SSO_ENABLED = Boolean(
import.meta.env.NEXT_PRIVATE_GOOGLE_CLIENT_ID &&
import.meta.env.NEXT_PRIVATE_GOOGLE_CLIENT_SECRET,
env('NEXT_PRIVATE_GOOGLE_CLIENT_ID') && env('NEXT_PRIVATE_GOOGLE_CLIENT_SECRET'),
);
export const IS_OIDC_SSO_ENABLED = Boolean(
process.env.NEXT_PRIVATE_OIDC_WELL_KNOWN &&
process.env.NEXT_PRIVATE_OIDC_CLIENT_ID &&
process.env.NEXT_PRIVATE_OIDC_CLIENT_SECRET,
env('NEXT_PRIVATE_OIDC_WELL_KNOWN') &&
env('NEXT_PRIVATE_OIDC_CLIENT_ID') &&
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> = {
ACCOUNT_SSO_LINK: 'Linked account to SSO',
@ -49,7 +48,7 @@ export const PASSKEY_TIMEOUT = 60000;
export const MAXIMUM_PASSKEYS = 50;
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-' : '';

View File

@ -1,4 +1,6 @@
export const FROM_ADDRESS = process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com';
export const FROM_NAME = process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso';
import { env } from '../utils/env';
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';

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';

View File

@ -17,8 +17,6 @@ export enum AppErrorCode {
'RETRY_EXCEPTION' = 'RETRY_EXCEPTION',
'SCHEMA_FAILED' = 'SCHEMA_FAILED',
'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 }> =
@ -34,8 +32,6 @@ export const genericErrorCodeToTrpcErrorCodeMap: Record<string, { code: string;
[AppErrorCode.RETRY_EXCEPTION]: { 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.PROFILE_URL_TAKEN]: { code: 'BAD_REQUEST', status: 400 },
[AppErrorCode.PREMIUM_PROFILE_URL]: { code: 'BAD_REQUEST', status: 400 },
};
export const ZAppErrorJsonSchema = z.object({

View File

@ -1,5 +1,6 @@
import { match } from 'ts-pattern';
import { env } from '../../utils/env';
import type { JobDefinition, TriggerJobOptions } from './_internal/job';
import type { BaseJobProvider as JobClientProvider } from './base';
import { InngestJobProvider } from './inngest';
@ -10,7 +11,7 @@ export class JobClient<T extends ReadonlyArray<JobDefinition> = []> {
private _provider: JobClientProvider;
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('trigger', () => TriggerJobProvider.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 { json } from 'micro';
import { env } from '../../utils/env';
import type { JobDefinition, JobRunIO, SimpleTriggerJobOptions } from './_internal/job';
import { BaseJobProvider } from './base';
@ -28,8 +29,8 @@ export class InngestJobProvider extends BaseJobProvider {
static getInstance() {
if (!this._instance) {
const client = new InngestClient({
id: process.env.NEXT_PRIVATE_INNGEST_APP_ID || 'documenso-app',
eventKey: process.env.INNGEST_EVENT_KEY || process.env.NEXT_PRIVATE_INNGEST_EVENT_KEY,
id: env('NEXT_PRIVATE_INNGEST_APP_ID') || 'documenso-app',
eventKey: env('INNGEST_EVENT_KEY') || env('NEXT_PRIVATE_INNGEST_EVENT_KEY'),
});
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 { TriggerClient, eventTrigger } from '@trigger.dev/sdk';
import { env } from '../../utils/env';
import type { JobDefinition, JobRunIO, SimpleTriggerJobOptions } from './_internal/job';
import { BaseJobProvider } from './base';
@ -20,8 +21,8 @@ export class TriggerJobProvider extends BaseJobProvider {
if (!this._instance) {
const client = new TriggerClient({
id: 'documenso-app',
apiKey: process.env.NEXT_PRIVATE_TRIGGER_API_KEY,
apiUrl: process.env.NEXT_PRIVATE_TRIGGER_API_URL,
apiKey: env('NEXT_PRIVATE_TRIGGER_API_KEY'),
apiUrl: env('NEXT_PRIVATE_TRIGGER_API_URL'),
});
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 type { GoogleProfile } 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 { formatSecureCookieName, useSecureCookies } from '../constants/auth';

View File

@ -1,24 +1,28 @@
export const isErrorCode = (code: unknown): code is ErrorCode => {
return typeof code === 'string' && code in ErrorCode;
};
// export const isErrorCode = (code: unknown): code is 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 = {
INCORRECT_EMAIL_PASSWORD: 'INCORRECT_EMAIL_PASSWORD',
USER_MISSING_PASSWORD: 'USER_MISSING_PASSWORD',
CREDENTIALS_NOT_FOUND: 'CREDENTIALS_NOT_FOUND',
INTERNAL_SEVER_ERROR: 'INTERNAL_SEVER_ERROR',
TWO_FACTOR_ALREADY_ENABLED: 'TWO_FACTOR_ALREADY_ENABLED',
TWO_FACTOR_SETUP_REQUIRED: 'TWO_FACTOR_SETUP_REQUIRED',
TWO_FACTOR_MISSING_SECRET: 'TWO_FACTOR_MISSING_SECRET',
TWO_FACTOR_MISSING_CREDENTIALS: 'TWO_FACTOR_MISSING_CREDENTIALS',
INCORRECT_TWO_FACTOR_CODE: 'INCORRECT_TWO_FACTOR_CODE',
INCORRECT_TWO_FACTOR_BACKUP_CODE: 'INCORRECT_TWO_FACTOR_BACKUP_CODE',
INCORRECT_IDENTITY_PROVIDER: 'INCORRECT_IDENTITY_PROVIDER',
INCORRECT_PASSWORD: 'INCORRECT_PASSWORD',
MISSING_ENCRYPTION_KEY: 'MISSING_ENCRYPTION_KEY',
MISSING_BACKUP_CODE: 'MISSING_BACKUP_CODE',
UNVERIFIED_EMAIL: 'UNVERIFIED_EMAIL',
ACCOUNT_DISABLED: 'ACCOUNT_DISABLED',
} as const;
// Todo: Delete file
// Todo: Delete file
// Todo: Delete file
// Todo: Delete file
// export const ErrorCode = {
// INCORRECT_EMAIL_PASSWORD: 'INCORRECT_EMAIL_PASSWORD',
// USER_MISSING_PASSWORD: 'USER_MISSING_PASSWORD',
// CREDENTIALS_NOT_FOUND: 'CREDENTIALS_NOT_FOUND',
// INTERNAL_SEVER_ERROR: 'INTERNAL_SEVER_ERROR',
// TWO_FACTOR_ALREADY_ENABLED: 'TWO_FACTOR_ALREADY_ENABLED',
// TWO_FACTOR_SETUP_REQUIRED: 'TWO_FACTOR_SETUP_REQUIRED',
// TWO_FACTOR_MISSING_SECRET: 'TWO_FACTOR_MISSING_SECRET',
// TWO_FACTOR_MISSING_CREDENTIALS: 'TWO_FACTOR_MISSING_CREDENTIALS',
// INCORRECT_TWO_FACTOR_CODE: 'INCORRECT_TWO_FACTOR_CODE',
// INCORRECT_TWO_FACTOR_BACKUP_CODE: 'INCORRECT_TWO_FACTOR_BACKUP_CODE',
// INCORRECT_IDENTITY_PROVIDER: 'INCORRECT_IDENTITY_PROVIDER',
// INCORRECT_PASSWORD: 'INCORRECT_PASSWORD',
// 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 { createTOTPKeyURI } from 'oslo/otp';
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
import { prisma } from '@documenso/prisma';
import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
@ -21,7 +20,7 @@ export const setupTwoFactorAuthentication = async ({
const key = DOCUMENSO_ENCRYPTION_KEY;
if (!key) {
throw new Error(ErrorCode.MISSING_ENCRYPTION_KEY);
throw new Error('MISSING_ENCRYPTION_KEY');
}
const secret = crypto.randomBytes(10);

View File

@ -1,6 +1,6 @@
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 { verifyBackupCode } from './verify-backup-code';
@ -16,11 +16,11 @@ export const validateTwoFactorAuthentication = async ({
user,
}: ValidateTwoFactorAuthenticationOptions) => {
if (!user.twoFactorEnabled) {
throw new Error(ErrorCode.TWO_FACTOR_SETUP_REQUIRED);
throw new AppError('TWO_FACTOR_SETUP_REQUIRED');
}
if (!user.twoFactorSecret) {
throw new Error(ErrorCode.TWO_FACTOR_MISSING_SECRET);
throw new AppError('TWO_FACTOR_MISSING_SECRET');
}
if (totpCode) {
@ -31,5 +31,5 @@ export const validateTwoFactorAuthentication = async ({
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 { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export interface SendConfirmationEmailProps {
@ -15,8 +16,8 @@ export interface 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_ADDRESS = process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS;
const NEXT_PRIVATE_SMTP_FROM_NAME = env('NEXT_PRIVATE_SMTP_FROM_NAME');
const NEXT_PRIVATE_SMTP_FROM_ADDRESS = env('NEXT_PRIVATE_SMTP_FROM_ADDRESS');
const user = await prisma.user.findFirstOrThrow({
where: {

View File

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

View File

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

View File

@ -14,6 +14,7 @@ import { extractDerivedDocumentEmailSettings } from '../../types/document-email'
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { getFile } from '../../universal/upload/get-file';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { env } from '../../utils/env';
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
@ -113,8 +114,8 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
},
],
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
},
subject: i18n._(msg`Signing Complete!`),
html,
@ -190,8 +191,8 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
},
],
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
},
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 { AppError, AppErrorCode } from '../../errors/app-error';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
@ -79,8 +80,8 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
name: name || '',
},
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
},
subject: i18n._(msg`Document Deleted!`),
html,

View File

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

View File

@ -3,6 +3,7 @@ import type { Browser } from 'playwright';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { type SupportedLanguageCodes, isValidLanguageCode } from '../../constants/i18n';
import { env } from '../../utils/env';
import { encryptSecondaryData } from '../crypto/encrypt';
export type GetCertificatePdfOptions = {
@ -21,10 +22,10 @@ export const getCertificatePdf = async ({ documentId, language }: GetCertificate
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.
// !: 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 {
browser = await chromium.launch();
}

View File

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

View File

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

View File

@ -1,7 +1,9 @@
/// <reference types="./stripe.d.ts" />
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',
typescript: true,
});

View File

@ -15,6 +15,7 @@ import { createTokenVerification } from '@documenso/lib/utils/token-verification
import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
@ -122,7 +123,7 @@ export const sendTeamEmailVerificationEmail = async (
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, {
assetBaseUrl,

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