mirror of
https://github.com/documenso/documenso.git
synced 2025-11-14 00:32:43 +10:00
fix: wip
This commit is contained in:
@ -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)} />;
|
||||
}
|
||||
@ -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)} />;
|
||||
}
|
||||
@ -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)} />;
|
||||
}
|
||||
@ -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],
|
||||
);
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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)}
|
||||
/>
|
||||
|
||||
@ -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;
|
||||
|
||||
155
apps/remix/app/components/dialogs/account-delete-dialog.tsx
Normal file
155
apps/remix/app/components/dialogs/account-delete-dialog.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
@ -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();
|
||||
@ -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();
|
||||
@ -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();
|
||||
@ -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();
|
||||
@ -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');
|
||||
@ -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();
|
||||
@ -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();
|
||||
@ -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(() => (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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`),
|
||||
|
||||
@ -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`),
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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';
|
||||
@ -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();
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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} /> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
165
apps/remix/app/routes/_authenticated+/documents+/_index.tsx
Normal file
165
apps/remix/app/routes/_authenticated+/documents+/_index.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
import DocumentPage, { loader } from '~/routes/_authenticated+/documents+/$id._index';
|
||||
|
||||
export { loader };
|
||||
|
||||
export default DocumentPage;
|
||||
@ -0,0 +1,5 @@
|
||||
import DocumentEditPage, { loader } from '~/routes/_authenticated+/documents+/$id.edit';
|
||||
|
||||
export { loader };
|
||||
|
||||
export default DocumentEditPage;
|
||||
@ -0,0 +1,5 @@
|
||||
import DocumentLogsPage, { loader } from '~/routes/_authenticated+/documents+/$id.logs';
|
||||
|
||||
export { loader };
|
||||
|
||||
export default DocumentLogsPage;
|
||||
@ -0,0 +1,5 @@
|
||||
import DocumentsPage, { meta } from '~/routes/_authenticated+/documents+/_index';
|
||||
|
||||
export { meta };
|
||||
|
||||
export default DocumentsPage;
|
||||
@ -0,0 +1,5 @@
|
||||
import DocumentsPage, { meta } from '~/routes/_authenticated+/documents+/_index';
|
||||
|
||||
export { meta };
|
||||
|
||||
export default DocumentsPage;
|
||||
@ -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">
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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",
|
||||
@ -48,4 +49,4 @@
|
||||
"vite-plugin-babel-macros": "^1.0.6",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
22
apps/remix/public/pdf.worker.min.js
vendored
Normal file
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
10
apps/remix/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_APP_TITLE: string;
|
||||
// more env variables...
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
@ -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,
|
||||
// },
|
||||
});
|
||||
|
||||
@ -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(() => (
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -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}`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -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);
|
||||
},
|
||||
)
|
||||
/**
|
||||
|
||||
@ -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',
|
||||
});
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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') } : {}),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -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 = {};
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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-' : '';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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');
|
||||
};
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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') ?? '',
|
||||
});
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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
Reference in New Issue
Block a user