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');
|
params.delete('search');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Todo: Test
|
void navigate(`?${params.toString()}`);
|
||||||
void navigate(`/documents?${params.toString()}`);
|
|
||||||
},
|
},
|
||||||
[searchParams],
|
[searchParams],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
|
|||||||
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||||
import { TableCell } from '@documenso/ui/primitives/table';
|
import { TableCell } from '@documenso/ui/primitives/table';
|
||||||
|
|
||||||
import { LeaveTeamDialog } from '../dialogs/leave-team-dialog';
|
import { TeamLeaveDialog } from '~/components/dialogs/team-leave-dialog';
|
||||||
|
|
||||||
export const CurrentUserTeamsDataTable = () => {
|
export const CurrentUserTeamsDataTable = () => {
|
||||||
const { _, i18n } = useLingui();
|
const { _, i18n } = useLingui();
|
||||||
@ -99,7 +99,7 @@ export const CurrentUserTeamsDataTable = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<LeaveTeamDialog
|
<TeamLeaveDialog
|
||||||
teamId={row.original.id}
|
teamId={row.original.id}
|
||||||
teamName={row.original.name}
|
teamName={row.original.name}
|
||||||
teamAvatarImageId={row.original.avatarImageId}
|
teamAvatarImageId={row.original.avatarImageId}
|
||||||
|
|||||||
@ -15,7 +15,8 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
|
|||||||
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||||
import { TableCell } from '@documenso/ui/primitives/table';
|
import { TableCell } from '@documenso/ui/primitives/table';
|
||||||
|
|
||||||
import { CreateTeamCheckoutDialog } from '../dialogs/create-team-checkout-dialog';
|
import { TeamCheckoutCreateDialog } from '~/components/dialogs/team-checkout-create-dialog';
|
||||||
|
|
||||||
import { PendingUserTeamsDataTableActions } from './pending-user-teams-data-table-actions';
|
import { PendingUserTeamsDataTableActions } from './pending-user-teams-data-table-actions';
|
||||||
|
|
||||||
export const PendingUserTeamsDataTable = () => {
|
export const PendingUserTeamsDataTable = () => {
|
||||||
@ -139,7 +140,7 @@ export const PendingUserTeamsDataTable = () => {
|
|||||||
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
|
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
|
||||||
</DataTable>
|
</DataTable>
|
||||||
|
|
||||||
<CreateTeamCheckoutDialog
|
<TeamCheckoutCreateDialog
|
||||||
pendingTeamId={checkoutPendingTeamId}
|
pendingTeamId={checkoutPendingTeamId}
|
||||||
onClose={() => setCheckoutPendingTeamId(null)}
|
onClose={() => setCheckoutPendingTeamId(null)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -26,8 +26,8 @@ import {
|
|||||||
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||||
import { TableCell } from '@documenso/ui/primitives/table';
|
import { TableCell } from '@documenso/ui/primitives/table';
|
||||||
|
|
||||||
import { DeleteTeamMemberDialog } from '../dialogs/delete-team-member-dialog';
|
import { DeleteTeamMemberDialog } from '../../dialogs/team-member-delete-dialog';
|
||||||
import { UpdateTeamMemberDialog } from '../dialogs/update-team-member-dialog';
|
import { UpdateTeamMemberDialog } from '../../dialogs/team-member-update-dialog';
|
||||||
|
|
||||||
export type TeamMembersDataTableProps = {
|
export type TeamMembersDataTableProps = {
|
||||||
currentUserTeamRole: TeamMemberRole;
|
currentUserTeamRole: TeamMemberRole;
|
||||||
|
|||||||
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 { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
type DocumentDuplicateDialogProps = {
|
type DocumentDuplicateDialogProps = {
|
||||||
id: number;
|
id: number;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (_open: boolean) => void;
|
onOpenChange: (_open: boolean) => void;
|
||||||
team?: Pick<Team, 'id' | 'url'>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentDuplicateDialog = ({
|
export const DocumentDuplicateDialog = ({
|
||||||
id,
|
id,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
team,
|
|
||||||
}: DocumentDuplicateDialogProps) => {
|
}: DocumentDuplicateDialogProps) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
|
const team = useOptionalCurrentTeam();
|
||||||
|
|
||||||
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery({
|
const { data: document, isLoading } = trpcReact.document.getDocumentById.useQuery({
|
||||||
documentId: id,
|
documentId: id,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -36,6 +36,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
import { StackAvatar } from '~/components/(dashboard)/avatar/stack-avatar';
|
import { StackAvatar } from '~/components/(dashboard)/avatar/stack-avatar';
|
||||||
import { useAuth } from '~/providers/auth';
|
import { useAuth } from '~/providers/auth';
|
||||||
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
const FORM_ID = 'resend-email';
|
const FORM_ID = 'resend-email';
|
||||||
|
|
||||||
@ -44,7 +45,6 @@ export type DocumentResendDialogProps = {
|
|||||||
team: Pick<Team, 'id' | 'url'> | null;
|
team: Pick<Team, 'id' | 'url'> | null;
|
||||||
};
|
};
|
||||||
recipients: Recipient[];
|
recipients: Recipient[];
|
||||||
team?: Pick<Team, 'id' | 'url'>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZResendDocumentFormSchema = z.object({
|
export const ZResendDocumentFormSchema = z.object({
|
||||||
@ -55,8 +55,9 @@ export const ZResendDocumentFormSchema = z.object({
|
|||||||
|
|
||||||
export type TResendDocumentFormSchema = z.infer<typeof ZResendDocumentFormSchema>;
|
export type TResendDocumentFormSchema = z.infer<typeof ZResendDocumentFormSchema>;
|
||||||
|
|
||||||
export const DocumentResendDialog = ({ document, recipients, team }: DocumentResendDialogProps) => {
|
export const DocumentResendDialog = ({ document, recipients }: DocumentResendDialogProps) => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const team = useOptionalCurrentTeam();
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|||||||
@ -20,18 +20,18 @@ import {
|
|||||||
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type CreateTeamCheckoutDialogProps = {
|
export type TeamCheckoutCreateDialogProps = {
|
||||||
pendingTeamId: number | null;
|
pendingTeamId: number | null;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||||
|
|
||||||
const MotionCard = motion(Card);
|
const MotionCard = motion(Card);
|
||||||
|
|
||||||
export const CreateTeamCheckoutDialog = ({
|
export const TeamCheckoutCreateDialog = ({
|
||||||
pendingTeamId,
|
pendingTeamId,
|
||||||
onClose,
|
onClose,
|
||||||
...props
|
...props
|
||||||
}: CreateTeamCheckoutDialogProps) => {
|
}: TeamCheckoutCreateDialogProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type CreateTeamDialogProps = {
|
export type TeamCreateDialogProps = {
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ const ZCreateTeamFormSchema = ZCreateTeamMutationSchema.pick({
|
|||||||
|
|
||||||
type TCreateTeamFormSchema = z.infer<typeof ZCreateTeamFormSchema>;
|
type TCreateTeamFormSchema = z.infer<typeof ZCreateTeamFormSchema>;
|
||||||
|
|
||||||
export const CreateTeamDialog = ({ trigger, ...props }: CreateTeamDialogProps) => {
|
export const TeamCreateDialog = ({ trigger, ...props }: TeamCreateDialogProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@ -30,13 +30,13 @@ import { Input } from '@documenso/ui/primitives/input';
|
|||||||
import type { Toast } from '@documenso/ui/primitives/use-toast';
|
import type { Toast } from '@documenso/ui/primitives/use-toast';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type DeleteTeamDialogProps = {
|
export type TeamDeleteDialogProps = {
|
||||||
teamId: number;
|
teamId: number;
|
||||||
teamName: string;
|
teamName: string;
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteTeamDialog = ({ trigger, teamId, teamName }: DeleteTeamDialogProps) => {
|
export const TeamDeleteDialog = ({ trigger, teamId, teamName }: TeamDeleteDialogProps) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type AddTeamEmailDialogProps = {
|
export type TeamEmailAddDialogProps = {
|
||||||
teamId: number;
|
teamId: number;
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||||
@ -44,7 +44,7 @@ const ZCreateTeamEmailFormSchema = ZCreateTeamEmailVerificationMutationSchema.pi
|
|||||||
|
|
||||||
type TCreateTeamEmailFormSchema = z.infer<typeof ZCreateTeamEmailFormSchema>;
|
type TCreateTeamEmailFormSchema = z.infer<typeof ZCreateTeamEmailFormSchema>;
|
||||||
|
|
||||||
export const AddTeamEmailDialog = ({ teamId, trigger, ...props }: AddTeamEmailDialogProps) => {
|
export const TeamEmailAddDialog = ({ teamId, trigger, ...props }: TeamEmailAddDialogProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@ -21,7 +21,7 @@ import {
|
|||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type RemoveTeamEmailDialogProps = {
|
export type TeamEmailDeleteDialogProps = {
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
teamName: string;
|
teamName: string;
|
||||||
team: Prisma.TeamGetPayload<{
|
team: Prisma.TeamGetPayload<{
|
||||||
@ -38,7 +38,7 @@ export type RemoveTeamEmailDialogProps = {
|
|||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RemoveTeamEmailDialog = ({ trigger, teamName, team }: RemoveTeamEmailDialogProps) => {
|
export const TeamEmailDeleteDialog = ({ trigger, teamName, team }: TeamEmailDeleteDialogProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@ -30,7 +30,7 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type UpdateTeamEmailDialogProps = {
|
export type TeamEmailUpdateDialogProps = {
|
||||||
teamEmail: TeamEmail;
|
teamEmail: TeamEmail;
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||||
@ -41,11 +41,11 @@ const ZUpdateTeamEmailFormSchema = z.object({
|
|||||||
|
|
||||||
type TUpdateTeamEmailFormSchema = z.infer<typeof ZUpdateTeamEmailFormSchema>;
|
type TUpdateTeamEmailFormSchema = z.infer<typeof ZUpdateTeamEmailFormSchema>;
|
||||||
|
|
||||||
export const UpdateTeamEmailDialog = ({
|
export const TeamEmailUpdateDialog = ({
|
||||||
teamEmail,
|
teamEmail,
|
||||||
trigger,
|
trigger,
|
||||||
...props
|
...props
|
||||||
}: UpdateTeamEmailDialogProps) => {
|
}: TeamEmailUpdateDialogProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@ -21,7 +21,7 @@ import {
|
|||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type LeaveTeamDialogProps = {
|
export type TeamLeaveDialogProps = {
|
||||||
teamId: number;
|
teamId: number;
|
||||||
teamName: string;
|
teamName: string;
|
||||||
teamAvatarImageId?: string | null;
|
teamAvatarImageId?: string | null;
|
||||||
@ -29,13 +29,13 @@ export type LeaveTeamDialogProps = {
|
|||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LeaveTeamDialog = ({
|
export const TeamLeaveDialog = ({
|
||||||
trigger,
|
trigger,
|
||||||
teamId,
|
teamId,
|
||||||
teamName,
|
teamName,
|
||||||
teamAvatarImageId,
|
teamAvatarImageId,
|
||||||
role,
|
role,
|
||||||
}: LeaveTeamDialogProps) => {
|
}: TeamLeaveDialogProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@ -18,7 +18,7 @@ import {
|
|||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type DeleteTeamMemberDialogProps = {
|
export type TeamMemberDeleteDialogProps = {
|
||||||
teamId: number;
|
teamId: number;
|
||||||
teamName: string;
|
teamName: string;
|
||||||
teamMemberId: number;
|
teamMemberId: number;
|
||||||
@ -27,14 +27,14 @@ export type DeleteTeamMemberDialogProps = {
|
|||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteTeamMemberDialog = ({
|
export const TeamMemberDeleteDialog = ({
|
||||||
trigger,
|
trigger,
|
||||||
teamId,
|
teamId,
|
||||||
teamName,
|
teamName,
|
||||||
teamMemberId,
|
teamMemberId,
|
||||||
teamMemberName,
|
teamMemberName,
|
||||||
teamMemberEmail,
|
teamMemberEmail,
|
||||||
}: DeleteTeamMemberDialogProps) => {
|
}: TeamMemberDeleteDialogProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@ -45,7 +45,7 @@ import {
|
|||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type InviteTeamMembersDialogProps = {
|
export type TeamMemberInviteDialogProps = {
|
||||||
currentUserTeamRole: TeamMemberRole;
|
currentUserTeamRole: TeamMemberRole;
|
||||||
teamId: number;
|
teamId: number;
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
@ -94,12 +94,12 @@ const ZImportTeamMemberSchema = z.array(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const InviteTeamMembersDialog = ({
|
export const TeamMemberInviteDialog = ({
|
||||||
currentUserTeamRole,
|
currentUserTeamRole,
|
||||||
teamId,
|
teamId,
|
||||||
trigger,
|
trigger,
|
||||||
...props
|
...props
|
||||||
}: InviteTeamMembersDialogProps) => {
|
}: TeamMemberInviteDialogProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [invitationType, setInvitationType] = useState<TabTypes>('INDIVIDUAL');
|
const [invitationType, setInvitationType] = useState<TabTypes>('INDIVIDUAL');
|
||||||
@ -38,7 +38,7 @@ import {
|
|||||||
} from '@documenso/ui/primitives/select';
|
} from '@documenso/ui/primitives/select';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type UpdateTeamMemberDialogProps = {
|
export type TeamMemberUpdateDialogProps = {
|
||||||
currentUserTeamRole: TeamMemberRole;
|
currentUserTeamRole: TeamMemberRole;
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
teamId: number;
|
teamId: number;
|
||||||
@ -53,7 +53,7 @@ const ZUpdateTeamMemberFormSchema = z.object({
|
|||||||
|
|
||||||
type ZUpdateTeamMemberSchema = z.infer<typeof ZUpdateTeamMemberFormSchema>;
|
type ZUpdateTeamMemberSchema = z.infer<typeof ZUpdateTeamMemberFormSchema>;
|
||||||
|
|
||||||
export const UpdateTeamMemberDialog = ({
|
export const TeamMemberUpdateDialog = ({
|
||||||
currentUserTeamRole,
|
currentUserTeamRole,
|
||||||
trigger,
|
trigger,
|
||||||
teamId,
|
teamId,
|
||||||
@ -61,7 +61,7 @@ export const UpdateTeamMemberDialog = ({
|
|||||||
teamMemberName,
|
teamMemberName,
|
||||||
teamMemberRole,
|
teamMemberRole,
|
||||||
...props
|
...props
|
||||||
}: UpdateTeamMemberDialogProps) => {
|
}: TeamMemberUpdateDialogProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@ -38,19 +38,19 @@ import {
|
|||||||
} from '@documenso/ui/primitives/select';
|
} from '@documenso/ui/primitives/select';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type TransferTeamDialogProps = {
|
export type TeamTransferDialogProps = {
|
||||||
teamId: number;
|
teamId: number;
|
||||||
teamName: string;
|
teamName: string;
|
||||||
ownerUserId: number;
|
ownerUserId: number;
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TransferTeamDialog = ({
|
export const TeamTransferDialog = ({
|
||||||
trigger,
|
trigger,
|
||||||
teamId,
|
teamId,
|
||||||
teamName,
|
teamName,
|
||||||
ownerUserId,
|
ownerUserId,
|
||||||
}: TransferTeamDialogProps) => {
|
}: TeamTransferDialogProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
@ -8,7 +8,6 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { downloadFile } from '@documenso/lib/client-only/download-file';
|
import { downloadFile } from '@documenso/lib/client-only/download-file';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -145,7 +144,7 @@ export const ViewRecoveryCodesDialog = () => {
|
|||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{match(AppError.parseError(error).message)
|
{match(AppError.parseError(error).message)
|
||||||
.with(ErrorCode.INCORRECT_TWO_FACTOR_CODE, () => (
|
.with('INCORRECT_TWO_FACTOR_CODE', () => (
|
||||||
<Trans>Invalid code. Please try again.</Trans>
|
<Trans>Invalid code. Please try again.</Trans>
|
||||||
))
|
))
|
||||||
.otherwise(() => (
|
.otherwise(() => (
|
||||||
|
|||||||
@ -3,14 +3,14 @@ import { useMemo } from 'react';
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { Trans, msg } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
// Todo
|
import { ErrorCode, useDropzone } from 'react-dropzone';
|
||||||
// import { ErrorCode, useDropzone } from 'react-dropzone';
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
import { base64 } from '@documenso/lib/universal/base64';
|
||||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@ -40,9 +40,9 @@ export type AvatarImageFormProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AvatarImageForm = ({ className }: AvatarImageFormProps) => {
|
export const AvatarImageForm = ({ className }: AvatarImageFormProps) => {
|
||||||
|
const { user } = useAuth();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { user } = useAuth();
|
|
||||||
|
|
||||||
const team = useOptionalCurrentTeam();
|
const team = useOptionalCurrentTeam();
|
||||||
|
|
||||||
@ -67,31 +67,31 @@ export const AvatarImageForm = ({ className }: AvatarImageFormProps) => {
|
|||||||
resolver: zodResolver(ZAvatarImageFormSchema),
|
resolver: zodResolver(ZAvatarImageFormSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
// const { getRootProps, getInputProps } = useDropzone({
|
const { getRootProps, getInputProps } = useDropzone({
|
||||||
// maxSize: 1024 * 1024,
|
maxSize: 1024 * 1024,
|
||||||
// accept: {
|
accept: {
|
||||||
// 'image/*': ['.png', '.jpg', '.jpeg'],
|
'image/*': ['.png', '.jpg', '.jpeg'],
|
||||||
// },
|
},
|
||||||
// multiple: false,
|
multiple: false,
|
||||||
// onDropAccepted: ([file]) => {
|
onDropAccepted: ([file]) => {
|
||||||
// void file.arrayBuffer().then((buffer) => {
|
void file.arrayBuffer().then((buffer) => {
|
||||||
// const contents = base64.encode(new Uint8Array(buffer));
|
const contents = base64.encode(new Uint8Array(buffer));
|
||||||
|
|
||||||
// form.setValue('bytes', contents);
|
form.setValue('bytes', contents);
|
||||||
// void form.handleSubmit(onFormSubmit)();
|
void form.handleSubmit(onFormSubmit)();
|
||||||
// });
|
});
|
||||||
// },
|
},
|
||||||
// onDropRejected: ([file]) => {
|
onDropRejected: ([file]) => {
|
||||||
// form.setError('bytes', {
|
form.setError('bytes', {
|
||||||
// type: 'onChange',
|
type: 'onChange',
|
||||||
// message: match(file.errors[0].code)
|
message: match(file.errors[0].code)
|
||||||
// .with(ErrorCode.FileTooLarge, () => _(msg`Uploaded file is too large`))
|
.with(ErrorCode.FileTooLarge, () => _(msg`Uploaded file is too large`))
|
||||||
// .with(ErrorCode.FileTooSmall, () => _(msg`Uploaded file is too small`))
|
.with(ErrorCode.FileTooSmall, () => _(msg`Uploaded file is too small`))
|
||||||
// .with(ErrorCode.FileInvalidType, () => _(msg`Uploaded file not an allowed file type`))
|
.with(ErrorCode.FileInvalidType, () => _(msg`Uploaded file not an allowed file type`))
|
||||||
// .otherwise(() => _(msg`An unknown error occurred`)),
|
.otherwise(() => _(msg`An unknown error occurred`)),
|
||||||
// });
|
});
|
||||||
// },
|
},
|
||||||
// });
|
});
|
||||||
|
|
||||||
const onFormSubmit = async (data: TAvatarImageFormSchema) => {
|
const onFormSubmit = async (data: TAvatarImageFormSchema) => {
|
||||||
try {
|
try {
|
||||||
@ -106,7 +106,8 @@ export const AvatarImageForm = ({ className }: AvatarImageFormProps) => {
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// router.refresh(); // Todo
|
// Todo
|
||||||
|
// router.refresh();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
@ -143,7 +144,11 @@ export const AvatarImageForm = ({ className }: AvatarImageFormProps) => {
|
|||||||
<div className="flex items-center gap-8">
|
<div className="flex items-center gap-8">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Avatar className="h-16 w-16 border-2 border-solid">
|
<Avatar className="h-16 w-16 border-2 border-solid">
|
||||||
{avatarImageId && <AvatarImage src={formatAvatarUrl(avatarImageId)} />}
|
{avatarImageId && (
|
||||||
|
<AvatarImage
|
||||||
|
src={`${NEXT_PUBLIC_WEBAPP_URL()}/api/avatar/${avatarImageId}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<AvatarFallback className="text-sm text-gray-400">
|
<AvatarFallback className="text-sm text-gray-400">
|
||||||
{initials}
|
{initials}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
@ -165,12 +170,12 @@ export const AvatarImageForm = ({ className }: AvatarImageFormProps) => {
|
|||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
// {...getRootProps()}
|
{...getRootProps()}
|
||||||
loading={form.formState.isSubmitting}
|
loading={form.formState.isSubmitting}
|
||||||
disabled={form.formState.isSubmitting}
|
disabled={form.formState.isSubmitting}
|
||||||
>
|
>
|
||||||
<Trans>Upload Avatar</Trans>
|
<Trans>Upload Avatar</Trans>
|
||||||
{/* <input {...getInputProps()} /> */}
|
<input {...getInputProps()} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
|
||||||
import { ZCurrentPasswordSchema, ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
import { ZCurrentPasswordSchema, ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -55,11 +55,9 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
|
|||||||
|
|
||||||
const isSubmitting = form.formState.isSubmitting;
|
const isSubmitting = form.formState.isSubmitting;
|
||||||
|
|
||||||
const { mutateAsync: updatePassword } = trpc.profile.updatePassword.useMutation();
|
|
||||||
|
|
||||||
const onFormSubmit = async ({ currentPassword, password }: TPasswordFormSchema) => {
|
const onFormSubmit = async ({ currentPassword, password }: TPasswordFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await updatePassword({
|
await authClient.updatePassword({
|
||||||
currentPassword,
|
currentPassword,
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -88,12 +88,12 @@ export const ClaimPublicProfileDialogForm = ({
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
if (error.code === AppErrorCode.PROFILE_URL_TAKEN) {
|
if (error.code === 'PROFILE_URL_TAKEN') {
|
||||||
form.setError('url', {
|
form.setError('url', {
|
||||||
type: 'manual',
|
type: 'manual',
|
||||||
message: _(msg`This username is already taken`),
|
message: _(msg`This username is already taken`),
|
||||||
});
|
});
|
||||||
} else if (error.code === AppErrorCode.PREMIUM_PROFILE_URL) {
|
} else if (error.code === 'PREMIUM_PROFILE_URL') {
|
||||||
form.setError('url', {
|
form.setError('url', {
|
||||||
type: 'manual',
|
type: 'manual',
|
||||||
message: error.message,
|
message: error.message,
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { useForm } from 'react-hook-form';
|
|||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { formatUserProfilePath } from '@documenso/lib/utils/public-profiles';
|
import { formatUserProfilePath } from '@documenso/lib/utils/public-profiles';
|
||||||
import {
|
import {
|
||||||
MAX_PROFILE_BIO_LENGTH,
|
MAX_PROFILE_BIO_LENGTH,
|
||||||
@ -88,8 +88,8 @@ export const PublicProfileForm = ({
|
|||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
switch (error.code) {
|
switch (error.code) {
|
||||||
case AppErrorCode.PREMIUM_PROFILE_URL:
|
case 'PREMIUM_PROFILE_URL':
|
||||||
case AppErrorCode.PROFILE_URL_TAKEN:
|
case 'PROFILE_URL_TAKEN':
|
||||||
form.setError('url', {
|
form.setError('url', {
|
||||||
type: 'manual',
|
type: 'manual',
|
||||||
message: error.message,
|
message: error.message,
|
||||||
|
|||||||
@ -15,7 +15,6 @@ import { z } from 'zod';
|
|||||||
import { authClient } from '@documenso/auth/client';
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { AuthenticationErrorCode } from '@documenso/auth/server/lib/errors/error-codes';
|
import { AuthenticationErrorCode } from '@documenso/auth/server/lib/errors/error-codes';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { ZCurrentPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
import { ZCurrentPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@ -46,8 +45,6 @@ const CommonErrorMessages = {
|
|||||||
[AuthenticationErrorCode.AccountDisabled]: msg`This account has been disabled. Please contact support.`,
|
[AuthenticationErrorCode.AccountDisabled]: msg`This account has been disabled. Please contact support.`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const TwoFactorEnabledErrorCode = ErrorCode.TWO_FACTOR_MISSING_CREDENTIALS;
|
|
||||||
|
|
||||||
const LOGIN_REDIRECT_PATH = '/documents';
|
const LOGIN_REDIRECT_PATH = '/documents';
|
||||||
|
|
||||||
export const ZSignInFormSchema = z.object({
|
export const ZSignInFormSchema = z.object({
|
||||||
@ -90,7 +87,7 @@ export const SignInForm = ({
|
|||||||
|
|
||||||
const [isPasskeyLoading, setIsPasskeyLoading] = useState(false);
|
const [isPasskeyLoading, setIsPasskeyLoading] = useState(false);
|
||||||
|
|
||||||
const callbackUrl = useMemo(() => {
|
const redirectUrl = useMemo(() => {
|
||||||
// Handle SSR
|
// Handle SSR
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return LOGIN_REDIRECT_PATH;
|
return LOGIN_REDIRECT_PATH;
|
||||||
@ -161,15 +158,16 @@ export const SignInForm = ({
|
|||||||
|
|
||||||
const credential = await startAuthentication(options);
|
const credential = await startAuthentication(options);
|
||||||
|
|
||||||
const result = await authClient.passkey.signIn({
|
await authClient.passkey.signIn({
|
||||||
credential: JSON.stringify(credential),
|
credential: JSON.stringify(credential),
|
||||||
csrfToken: sessionId,
|
csrfToken: sessionId,
|
||||||
|
redirectUrl,
|
||||||
// callbackUrl,
|
// callbackUrl,
|
||||||
// redirect: false,
|
// redirect: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Todo: Can't use navigate because of embed?
|
// Todo: Can't use navigate because of embed?
|
||||||
window.location.href = callbackUrl;
|
// window.location.href = callbackUrl;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setIsPasskeyLoading(false);
|
setIsPasskeyLoading(false);
|
||||||
|
|
||||||
@ -208,17 +206,18 @@ export const SignInForm = ({
|
|||||||
password,
|
password,
|
||||||
totpCode,
|
totpCode,
|
||||||
backupCode,
|
backupCode,
|
||||||
|
redirectUrl,
|
||||||
// callbackUrl,
|
// callbackUrl,
|
||||||
// redirect: false,
|
// redirect: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
window.location.href = callbackUrl;
|
// window.location.href = callbackUrl; // Todo: Handle redirect.
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
|
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
if (error.code === TwoFactorEnabledErrorCode) {
|
if (error.code === 'TWO_FACTOR_MISSING_CREDENTIALS') {
|
||||||
setIsTwoFactorAuthenticationDialogOpen(true);
|
setIsTwoFactorAuthenticationDialogOpen(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -257,12 +256,7 @@ export const SignInForm = ({
|
|||||||
|
|
||||||
const onSignInWithGoogleClick = async () => {
|
const onSignInWithGoogleClick = async () => {
|
||||||
try {
|
try {
|
||||||
// await signIn('google', {
|
await authClient.google.signIn(); // Todo: Handle redirect.
|
||||||
// callbackUrl,
|
|
||||||
// });
|
|
||||||
|
|
||||||
const result = await authClient.google.signIn();
|
|
||||||
console.log(result);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`An unknown error occurred`),
|
||||||
|
|||||||
@ -12,9 +12,9 @@ import { Link, useNavigate, useSearchParams } from 'react-router';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import communityCardsImage from '@documenso/assets/images/community-cards.png';
|
import communityCardsImage from '@documenso/assets/images/community-cards.png';
|
||||||
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
|
||||||
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -71,8 +71,8 @@ export const signupErrorMessages: Record<string, MessageDescriptor> = {
|
|||||||
SIGNUP_DISABLED: msg`Signups are disabled.`,
|
SIGNUP_DISABLED: msg`Signups are disabled.`,
|
||||||
[AppErrorCode.ALREADY_EXISTS]: msg`User with this email already exists. Please use a different email address.`,
|
[AppErrorCode.ALREADY_EXISTS]: msg`User with this email already exists. Please use a different email address.`,
|
||||||
[AppErrorCode.INVALID_REQUEST]: msg`We were unable to create your account. Please review the information you provided and try again.`,
|
[AppErrorCode.INVALID_REQUEST]: msg`We were unable to create your account. Please review the information you provided and try again.`,
|
||||||
[AppErrorCode.PROFILE_URL_TAKEN]: msg`This username has already been taken`,
|
PROFILE_URL_TAKEN: msg`This username has already been taken`,
|
||||||
[AppErrorCode.PREMIUM_PROFILE_URL]: msg`Only subscribers can have a username shorter than 6 characters`,
|
PREMIUM_PROFILE_URL: msg`Only subscribers can have a username shorter than 6 characters`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSignUpFormSchema = z.infer<typeof ZSignUpFormSchema>;
|
export type TSignUpFormSchema = z.infer<typeof ZSignUpFormSchema>;
|
||||||
@ -93,7 +93,7 @@ export const SignUpForm = ({
|
|||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
// const analytics = useAnalytics();
|
// const analytics = useAnalytics(); // Todo
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
@ -120,11 +120,9 @@ export const SignUpForm = ({
|
|||||||
const name = form.watch('name');
|
const name = form.watch('name');
|
||||||
const url = form.watch('url');
|
const url = form.watch('url');
|
||||||
|
|
||||||
const { mutateAsync: signup } = trpc.auth.signup.useMutation();
|
|
||||||
|
|
||||||
const onFormSubmit = async ({ name, email, password, signature, url }: TSignUpFormSchema) => {
|
const onFormSubmit = async ({ name, email, password, signature, url }: TSignUpFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await signup({ name, email, password, signature, url });
|
await authClient.emailPassword.signUp({ name, email, password, signature, url });
|
||||||
|
|
||||||
void navigate(`/unverified-account`);
|
void navigate(`/unverified-account`);
|
||||||
|
|
||||||
@ -146,10 +144,7 @@ export const SignUpForm = ({
|
|||||||
|
|
||||||
const errorMessage = signupErrorMessages[error.code] ?? signupErrorMessages.INVALID_REQUEST;
|
const errorMessage = signupErrorMessages[error.code] ?? signupErrorMessages.INVALID_REQUEST;
|
||||||
|
|
||||||
if (
|
if (error.code === 'PROFILE_URL_TAKEN' || error.code === 'PREMIUM_PROFILE_URL') {
|
||||||
error.code === AppErrorCode.PROFILE_URL_TAKEN ||
|
|
||||||
error.code === AppErrorCode.PREMIUM_PROFILE_URL
|
|
||||||
) {
|
|
||||||
form.setError('url', {
|
form.setError('url', {
|
||||||
type: 'manual',
|
type: 'manual',
|
||||||
message: _(errorMessage),
|
message: _(errorMessage),
|
||||||
@ -175,8 +170,7 @@ export const SignUpForm = ({
|
|||||||
|
|
||||||
const onSignUpWithGoogleClick = async () => {
|
const onSignUpWithGoogleClick = async () => {
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
await authClient.google.signIn();
|
||||||
// await signIn('google', { callbackUrl: SIGN_UP_REDIRECT_PATH });
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`An unknown error occurred`),
|
title: _(msg`An unknown error occurred`),
|
||||||
|
|||||||
@ -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 { Button } from '@documenso/ui/primitives/button';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type DownloadAuditLogButtonProps = {
|
export type DocumentAuditLogDownloadButtonProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
teamId?: number;
|
|
||||||
documentId: number;
|
documentId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DownloadAuditLogButton = ({ className, documentId }: DownloadAuditLogButtonProps) => {
|
export const DocumentAuditLogDownloadButton = ({
|
||||||
|
className,
|
||||||
|
documentId,
|
||||||
|
}: DocumentAuditLogDownloadButtonProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
@ -1,5 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { Trans, msg } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { DocumentStatus } from '@prisma/client';
|
import { DocumentStatus } from '@prisma/client';
|
||||||
@ -10,19 +8,17 @@ import { cn } from '@documenso/ui/lib/utils';
|
|||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type DownloadCertificateButtonProps = {
|
export type DocumentCertificateDownloadButtonProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
documentId: number;
|
documentId: number;
|
||||||
documentStatus: DocumentStatus;
|
documentStatus: DocumentStatus;
|
||||||
teamId?: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DownloadCertificateButton = ({
|
export const DocumentCertificateDownloadButton = ({
|
||||||
className,
|
className,
|
||||||
documentId,
|
documentId,
|
||||||
documentStatus,
|
documentStatus,
|
||||||
teamId,
|
}: DocumentCertificateDownloadButtonProps) => {
|
||||||
}: DownloadCertificateButtonProps) => {
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
@ -3,6 +3,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { msg } from '@lingui/macro';
|
import { msg } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { DocumentDistributionMethod, DocumentStatus } from '@prisma/client';
|
import { DocumentDistributionMethod, DocumentStatus } from '@prisma/client';
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router';
|
||||||
|
|
||||||
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
|
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
|
||||||
import {
|
import {
|
||||||
@ -29,7 +30,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
export type EditDocumentFormProps = {
|
export type DocumentEditFormProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
initialDocument: TDocument;
|
initialDocument: TDocument;
|
||||||
documentRootPath: string;
|
documentRootPath: string;
|
||||||
@ -39,17 +40,18 @@ export type EditDocumentFormProps = {
|
|||||||
type EditDocumentStep = 'settings' | 'signers' | 'fields' | 'subject';
|
type EditDocumentStep = 'settings' | 'signers' | 'fields' | 'subject';
|
||||||
const EditDocumentSteps: EditDocumentStep[] = ['settings', 'signers', 'fields', 'subject'];
|
const EditDocumentSteps: EditDocumentStep[] = ['settings', 'signers', 'fields', 'subject'];
|
||||||
|
|
||||||
export const EditDocumentForm = ({
|
export const DocumentEditForm = ({
|
||||||
className,
|
className,
|
||||||
initialDocument,
|
initialDocument,
|
||||||
documentRootPath,
|
documentRootPath,
|
||||||
isDocumentEnterprise,
|
isDocumentEnterprise,
|
||||||
}: EditDocumentFormProps) => {
|
}: DocumentEditFormProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
const searchParams = useSearchParams();
|
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
const team = useOptionalCurrentTeam();
|
const team = useOptionalCurrentTeam();
|
||||||
|
|
||||||
const [isDocumentPdfLoaded, setIsDocumentPdfLoaded] = useState(false);
|
const [isDocumentPdfLoaded, setIsDocumentPdfLoaded] = useState(false);
|
||||||
@ -194,9 +196,6 @@ export const EditDocumentForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Router refresh is here to clear the router cache for when navigating to /documents.
|
|
||||||
router.refresh();
|
|
||||||
|
|
||||||
setStep('signers');
|
setStep('signers');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -227,9 +226,6 @@ export const EditDocumentForm = ({
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Router refresh is here to clear the router cache for when navigating to /documents.
|
|
||||||
router.refresh();
|
|
||||||
|
|
||||||
setStep('fields');
|
setStep('fields');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -265,9 +261,6 @@ export const EditDocumentForm = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Router refresh is here to clear the router cache for when navigating to /documents.
|
|
||||||
router.refresh();
|
|
||||||
|
|
||||||
setStep('subject');
|
setStep('subject');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -301,7 +294,7 @@ export const EditDocumentForm = ({
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push(documentRootPath);
|
void navigate(documentRootPath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,7 +305,7 @@ export const EditDocumentForm = ({
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
router.push(`${documentRootPath}/${document.id}`);
|
void navigate(`${documentRootPath}/${document.id}`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -3,7 +3,7 @@ import { useLingui } from '@lingui/react';
|
|||||||
import type { Document, Recipient, Team, User } from '@prisma/client';
|
import type { Document, Recipient, Team, User } from '@prisma/client';
|
||||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
||||||
import { CheckCircle, Download, EyeIcon, Pencil } from 'lucide-react';
|
import { CheckCircle, Download, EyeIcon, Pencil } from 'lucide-react';
|
||||||
import { useSession } from 'next-auth/react';
|
import { Link } from 'react-router';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
||||||
@ -12,25 +12,23 @@ import { trpc as trpcClient } from '@documenso/trpc/client';
|
|||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { useAuth } from '~/providers/auth';
|
||||||
|
|
||||||
export type DocumentPageViewButtonProps = {
|
export type DocumentPageViewButtonProps = {
|
||||||
document: Document & {
|
document: Document & {
|
||||||
user: Pick<User, 'id' | 'name' | 'email'>;
|
user: Pick<User, 'id' | 'name' | 'email'>;
|
||||||
recipients: Recipient[];
|
recipients: Recipient[];
|
||||||
team: Pick<Team, 'id' | 'url'> | null;
|
team: Pick<Team, 'id' | 'url'> | null;
|
||||||
};
|
};
|
||||||
team?: Pick<Team, 'id' | 'url'>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps) => {
|
export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps) => {
|
||||||
const { data: session } = useSession();
|
const { user } = useAuth();
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
if (!session) {
|
const recipient = document.recipients.find((recipient) => recipient.email === user.email);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipient = document.recipients.find((recipient) => recipient.email === session.user.email);
|
|
||||||
|
|
||||||
const isRecipient = !!recipient;
|
const isRecipient = !!recipient;
|
||||||
const isPending = document.status === DocumentStatus.PENDING;
|
const isPending = document.status === DocumentStatus.PENDING;
|
||||||
@ -77,7 +75,7 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
|
|||||||
})
|
})
|
||||||
.with({ isRecipient: true, isPending: true, isSigned: false }, () => (
|
.with({ isRecipient: true, isPending: true, isSigned: false }, () => (
|
||||||
<Button className="w-full" asChild>
|
<Button className="w-full" asChild>
|
||||||
<Link href={`/sign/${recipient?.token}`}>
|
<Link to={`/sign/${recipient?.token}`}>
|
||||||
{match(role)
|
{match(role)
|
||||||
.with(RecipientRole.SIGNER, () => (
|
.with(RecipientRole.SIGNER, () => (
|
||||||
<>
|
<>
|
||||||
@ -102,7 +100,7 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
|
|||||||
))
|
))
|
||||||
.with({ isComplete: false }, () => (
|
.with({ isComplete: false }, () => (
|
||||||
<Button className="w-full" asChild>
|
<Button className="w-full" asChild>
|
||||||
<Link href={`${documentsPath}/${document.id}/edit`}>
|
<Link to={`${documentsPath}/${document.id}/edit`}>
|
||||||
<Trans>Edit</Trans>
|
<Trans>Edit</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
@ -3,7 +3,7 @@ import { useState } from 'react';
|
|||||||
import { Trans, msg } from '@lingui/macro';
|
import { Trans, msg } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { DocumentStatus } from '@prisma/client';
|
import { DocumentStatus } from '@prisma/client';
|
||||||
import type { Document, Recipient, Team, TeamEmail, User } from '@prisma/client';
|
import type { Document, Recipient, Team, User } from '@prisma/client';
|
||||||
import {
|
import {
|
||||||
Copy,
|
Copy,
|
||||||
Download,
|
Download,
|
||||||
@ -14,7 +14,7 @@ import {
|
|||||||
Share,
|
Share,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useSession } from 'next-auth/react';
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
@ -29,11 +29,12 @@ import {
|
|||||||
} from '@documenso/ui/primitives/dropdown-menu';
|
} from '@documenso/ui/primitives/dropdown-menu';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { DocumentDeleteDialog } from '~/components/dialogs/document-delete-dialog';
|
||||||
|
import { DocumentDuplicateDialog } from '~/components/dialogs/document-duplicate-dialog';
|
||||||
|
import { DocumentResendDialog } from '~/components/dialogs/document-resend-dialog';
|
||||||
import { DocumentRecipientLinkCopyDialog } from '~/components/document/document-recipient-link-copy-dialog';
|
import { DocumentRecipientLinkCopyDialog } from '~/components/document/document-recipient-link-copy-dialog';
|
||||||
|
import { useAuth } from '~/providers/auth';
|
||||||
import { DeleteDocumentDialog } from '../delete-document-dialog';
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
import { DuplicateDocumentDialog } from '../duplicate-document-dialog';
|
|
||||||
import { ResendDocumentActionItem } from '../resend-document-dialog';
|
|
||||||
|
|
||||||
export type DocumentPageViewDropdownProps = {
|
export type DocumentPageViewDropdownProps = {
|
||||||
document: Document & {
|
document: Document & {
|
||||||
@ -41,24 +42,21 @@ export type DocumentPageViewDropdownProps = {
|
|||||||
recipients: Recipient[];
|
recipients: Recipient[];
|
||||||
team: Pick<Team, 'id' | 'url'> | null;
|
team: Pick<Team, 'id' | 'url'> | null;
|
||||||
};
|
};
|
||||||
team?: Pick<Team, 'id' | 'url'> & { teamEmail: TeamEmail | null };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDropdownProps) => {
|
export const DocumentPageViewDropdown = ({ document }: DocumentPageViewDropdownProps) => {
|
||||||
const { data: session } = useSession();
|
const { user } = useAuth();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
|
const team = useOptionalCurrentTeam();
|
||||||
|
|
||||||
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const [isDuplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
|
const [isDuplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
|
||||||
|
|
||||||
if (!session) {
|
const recipient = document.recipients.find((recipient) => recipient.email === user.email);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipient = document.recipients.find((recipient) => recipient.email === session.user.email);
|
const isOwner = document.user.id === user.id;
|
||||||
|
|
||||||
const isOwner = document.user.id === session.user.id;
|
|
||||||
const isDraft = document.status === DocumentStatus.DRAFT;
|
const isDraft = document.status === DocumentStatus.DRAFT;
|
||||||
const isPending = document.status === DocumentStatus.PENDING;
|
const isPending = document.status === DocumentStatus.PENDING;
|
||||||
const isDeleted = document.deletedAt !== null;
|
const isDeleted = document.deletedAt !== null;
|
||||||
@ -112,7 +110,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
|
|||||||
|
|
||||||
{(isOwner || isCurrentTeamDocument) && !isComplete && (
|
{(isOwner || isCurrentTeamDocument) && !isComplete && (
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href={`${documentsPath}/${document.id}/edit`}>
|
<Link to={`${documentsPath}/${document.id}/edit`}>
|
||||||
<Edit className="mr-2 h-4 w-4" />
|
<Edit className="mr-2 h-4 w-4" />
|
||||||
<Trans>Edit</Trans>
|
<Trans>Edit</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
@ -127,7 +125,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href={`${documentsPath}/${document.id}/logs`}>
|
<Link to={`${documentsPath}/${document.id}/logs`}>
|
||||||
<ScrollTextIcon className="mr-2 h-4 w-4" />
|
<ScrollTextIcon className="mr-2 h-4 w-4" />
|
||||||
<Trans>Audit Log</Trans>
|
<Trans>Audit Log</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
@ -165,11 +163,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ResendDocumentActionItem
|
<DocumentResendDialog document={document} recipients={nonSignedRecipients} />
|
||||||
document={document}
|
|
||||||
recipients={nonSignedRecipients}
|
|
||||||
team={team}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DocumentShareButton
|
<DocumentShareButton
|
||||||
documentId={document.id}
|
documentId={document.id}
|
||||||
@ -185,7 +179,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
|
|||||||
/>
|
/>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|
||||||
<DeleteDocumentDialog
|
<DocumentDeleteDialog
|
||||||
id={document.id}
|
id={document.id}
|
||||||
status={document.status}
|
status={document.status}
|
||||||
documentTitle={document.title}
|
documentTitle={document.title}
|
||||||
@ -195,11 +189,10 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{isDuplicateDialogOpen && (
|
{isDuplicateDialogOpen && (
|
||||||
<DuplicateDocumentDialog
|
<DocumentDuplicateDialog
|
||||||
id={document.id}
|
id={document.id}
|
||||||
open={isDuplicateDialogOpen}
|
open={isDuplicateDialogOpen}
|
||||||
onOpenChange={setDuplicateDialogOpen}
|
onOpenChange={setDuplicateDialogOpen}
|
||||||
team={team}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
@ -1,3 +1,7 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { Trans, msg } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
||||||
@ -1,5 +1,9 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
import { msg } from '@lingui/macro';
|
import { msg } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
@ -16,7 +20,7 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
|
|||||||
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||||
import { TableCell } from '@documenso/ui/primitives/table';
|
import { TableCell } from '@documenso/ui/primitives/table';
|
||||||
|
|
||||||
export type DocumentLogsDataTableProps = {
|
export type DocumentLogsTableProps = {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -25,7 +29,7 @@ const dateFormat: DateTimeFormatOptions = {
|
|||||||
hourCycle: 'h12',
|
hourCycle: 'h12',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentLogsDataTable = ({ documentId }: DocumentLogsDataTableProps) => {
|
export const DocumentLogsTable = ({ documentId }: DocumentLogsTableProps) => {
|
||||||
const { _, i18n } = useLingui();
|
const { _, i18n } = useLingui();
|
||||||
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@ -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,
|
Scripts,
|
||||||
ScrollRestoration,
|
ScrollRestoration,
|
||||||
isRouteErrorResponse,
|
isRouteErrorResponse,
|
||||||
|
useLoaderData,
|
||||||
} from 'react-router';
|
} from 'react-router';
|
||||||
|
|
||||||
import { TrpcProvider } from '@documenso/trpc/react';
|
import { TrpcProvider } from '@documenso/trpc/react';
|
||||||
@ -32,7 +33,17 @@ export const links: Route.LinksFunction = () => [
|
|||||||
{ rel: 'stylesheet', href: stylesheet },
|
{ rel: 'stylesheet', href: stylesheet },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export function loader() {
|
||||||
|
return {
|
||||||
|
__ENV__: Object.fromEntries(
|
||||||
|
Object.entries(process.env).filter(([key]) => key.startsWith('NEXT_')),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function Layout({ children }: { children: React.ReactNode }) {
|
export function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
const { __ENV__ } = useLoaderData<typeof loader>() || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
@ -45,6 +56,12 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
{children}
|
{children}
|
||||||
<ScrollRestoration />
|
<ScrollRestoration />
|
||||||
<Scripts />
|
<Scripts />
|
||||||
|
|
||||||
|
<script
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `window.__ENV__ = ${JSON.stringify(__ENV__)}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import {
|
|||||||
getUsersWithSubscriptionsCount,
|
getUsersWithSubscriptionsCount,
|
||||||
} from '@documenso/lib/server-only/admin/get-users-stats';
|
} from '@documenso/lib/server-only/admin/get-users-stats';
|
||||||
import { getSignerConversionMonthly } from '@documenso/lib/server-only/user/get-signer-conversion';
|
import { getSignerConversionMonthly } from '@documenso/lib/server-only/user/get-signer-conversion';
|
||||||
|
import { env } from '@documenso/lib/utils/env';
|
||||||
|
|
||||||
import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card';
|
import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card';
|
||||||
import { AdminStatsSignerConversionChart } from '~/components/general/admin-stats-signer-conversion-chart';
|
import { AdminStatsSignerConversionChart } from '~/components/general/admin-stats-signer-conversion-chart';
|
||||||
@ -87,11 +88,7 @@ export default function AdminStatsPage({ loaderData }: Route.ComponentProps) {
|
|||||||
value={usersWithSubscriptionsCount}
|
value={usersWithSubscriptionsCount}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CardMetric
|
<CardMetric icon={FileCog} title={_(msg`App Version`)} value={`v${env('APP_VERSION')}`} />
|
||||||
icon={FileCog}
|
|
||||||
title={_(msg`App Version`)}
|
|
||||||
value={`v${process.env.APP_VERSION}`}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-16 gap-8">
|
<div className="mt-16 gap-8">
|
||||||
|
|||||||
@ -1,20 +1,20 @@
|
|||||||
import { Plural, Trans } from '@lingui/macro';
|
import { Plural, Trans } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { DocumentStatus } from '@prisma/client';
|
import { DocumentStatus } from '@prisma/client';
|
||||||
import type { Team, TeamEmail } from '@prisma/client';
|
|
||||||
import { TeamMemberRole } from '@prisma/client';
|
import { TeamMemberRole } from '@prisma/client';
|
||||||
import { ChevronLeft, Clock9, Users2 } from 'lucide-react';
|
import { ChevronLeft, Clock9, Users2 } from 'lucide-react';
|
||||||
|
import { Link, redirect } from 'react-router';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
|
import { getRequiredSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||||
import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
|
import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
|
||||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||||
import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
|
|
||||||
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
|
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
|
||||||
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
||||||
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||||
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
import { Badge } from '@documenso/ui/primitives/badge';
|
import { Badge } from '@documenso/ui/primitives/badge';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
@ -28,24 +28,38 @@ import {
|
|||||||
DocumentStatus as DocumentStatusComponent,
|
DocumentStatus as DocumentStatusComponent,
|
||||||
FRIENDLY_STATUS_MAP,
|
FRIENDLY_STATUS_MAP,
|
||||||
} from '~/components/formatter/document-status';
|
} from '~/components/formatter/document-status';
|
||||||
|
import { DocumentPageViewButton } from '~/components/pages/document/document-page-view-button';
|
||||||
|
import { DocumentPageViewDropdown } from '~/components/pages/document/document-page-view-dropdown';
|
||||||
|
import { DocumentPageViewInformation } from '~/components/pages/document/document-page-view-information';
|
||||||
|
import { DocumentPageViewRecentActivity } from '~/components/pages/document/document-page-view-recent-activity';
|
||||||
|
import { DocumentPageViewRecipients } from '~/components/pages/document/document-page-view-recipients';
|
||||||
|
import { useAuth } from '~/providers/auth';
|
||||||
|
|
||||||
import { DocumentPageViewButton } from './document-page-view-button';
|
import type { Route } from './+types/$id._index';
|
||||||
import { DocumentPageViewDropdown } from './document-page-view-dropdown';
|
|
||||||
import { DocumentPageViewInformation } from './document-page-view-information';
|
|
||||||
import { DocumentPageViewRecentActivity } from './document-page-view-recent-activity';
|
|
||||||
import { DocumentPageViewRecipients } from './document-page-view-recipients';
|
|
||||||
|
|
||||||
export type DocumentPageViewProps = {
|
export async function loader({ request, params }: Route.LoaderArgs) {
|
||||||
documentId: number;
|
const { id } = params;
|
||||||
team?: Team & { teamEmail: TeamEmail | null } & { currentTeamMember: { role: TeamMemberRole } };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DocumentPageView = async ({ documentId, team }: DocumentPageViewProps) => {
|
const { user } = await getRequiredSession(request);
|
||||||
const { _ } = useLingui();
|
|
||||||
|
// Todo: Get from parent loader, this is just for testing.
|
||||||
|
const team = await prisma.team.findFirst({
|
||||||
|
where: {
|
||||||
|
documents: {
|
||||||
|
some: {
|
||||||
|
id: Number(id),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const documentId = Number(id);
|
||||||
|
|
||||||
const documentRootPath = formatDocumentsPath(team?.url);
|
const documentRootPath = formatDocumentsPath(team?.url);
|
||||||
|
|
||||||
const { user } = await getRequiredServerComponentSession();
|
if (!documentId || Number.isNaN(documentId)) {
|
||||||
|
return redirect(documentRootPath);
|
||||||
|
}
|
||||||
|
|
||||||
const document = await getDocumentById({
|
const document = await getDocumentById({
|
||||||
documentId,
|
documentId,
|
||||||
@ -54,7 +68,7 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
|
|||||||
}).catch(() => null);
|
}).catch(() => null);
|
||||||
|
|
||||||
if (document?.teamId && !team?.url) {
|
if (document?.teamId && !team?.url) {
|
||||||
redirect(documentRootPath);
|
return redirect(documentRootPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
const documentVisibility = document?.visibility;
|
const documentVisibility = document?.visibility;
|
||||||
@ -73,19 +87,15 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
|
|||||||
.otherwise(() => false);
|
.otherwise(() => false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDocumentHistoryEnabled = await getServerComponentFlag(
|
|
||||||
'app_document_page_view_history_sheet',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!document || !document.documentData || (team && !canAccessDocument)) {
|
if (!document || !document.documentData || (team && !canAccessDocument)) {
|
||||||
redirect(documentRootPath);
|
return redirect(documentRootPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (team && !canAccessDocument) {
|
if (team && !canAccessDocument) {
|
||||||
redirect(documentRootPath);
|
return redirect(documentRootPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { documentData, documentMeta } = document;
|
const { documentMeta } = document;
|
||||||
|
|
||||||
if (documentMeta?.password) {
|
if (documentMeta?.password) {
|
||||||
const key = DOCUMENSO_ENCRYPTION_KEY;
|
const key = DOCUMENSO_ENCRYPTION_KEY;
|
||||||
@ -104,6 +114,7 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
|
|||||||
documentMeta.password = securePassword;
|
documentMeta.password = securePassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Todo: Get full document instead???
|
||||||
const [recipients, fields] = await Promise.all([
|
const [recipients, fields] = await Promise.all([
|
||||||
getRecipientsForDocument({
|
getRecipientsForDocument({
|
||||||
documentId,
|
documentId,
|
||||||
@ -122,13 +133,30 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
|
|||||||
recipients,
|
recipients,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
document: documentWithRecipients,
|
||||||
|
documentRootPath,
|
||||||
|
fields,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DocumentPage({ loaderData }: Route.ComponentProps) {
|
||||||
|
const { _ } = useLingui();
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
const { document, documentRootPath, fields } = loaderData;
|
||||||
|
|
||||||
|
const { recipients, documentData, documentMeta } = document;
|
||||||
|
|
||||||
|
const isDocumentHistoryEnabled = false; // Todo: Was flag
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
|
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
|
||||||
{document.status === DocumentStatus.PENDING && (
|
{document.status === DocumentStatus.PENDING && (
|
||||||
<DocumentRecipientLinkCopyDialog recipients={recipients} />
|
<DocumentRecipientLinkCopyDialog recipients={recipients} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Link href={documentRootPath} className="flex items-center text-[#7AC455] hover:opacity-80">
|
<Link to={documentRootPath} className="flex items-center text-[#7AC455] hover:opacity-80">
|
||||||
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
||||||
<Trans>Documents</Trans>
|
<Trans>Documents</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
@ -207,7 +235,7 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
|
|||||||
{_(FRIENDLY_STATUS_MAP[document.status].labelExtended)}
|
{_(FRIENDLY_STATUS_MAP[document.status].labelExtended)}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<DocumentPageViewDropdown document={documentWithRecipients} team={team} />
|
<DocumentPageViewDropdown document={document} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 px-4 text-sm">
|
<p className="text-muted-foreground mt-2 px-4 text-sm">
|
||||||
@ -235,18 +263,15 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-4 border-t px-4 pt-4">
|
<div className="mt-4 border-t px-4 pt-4">
|
||||||
<DocumentPageViewButton document={documentWithRecipients} team={team} />
|
<DocumentPageViewButton document={document} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Document information section. */}
|
{/* Document information section. */}
|
||||||
<DocumentPageViewInformation document={documentWithRecipients} userId={user.id} />
|
<DocumentPageViewInformation document={document} userId={user.id} />
|
||||||
|
|
||||||
{/* Recipients section. */}
|
{/* Recipients section. */}
|
||||||
<DocumentPageViewRecipients
|
<DocumentPageViewRecipients document={document} documentRootPath={documentRootPath} />
|
||||||
document={documentWithRecipients}
|
|
||||||
documentRootPath={documentRootPath}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Recent activity section. */}
|
{/* Recent activity section. */}
|
||||||
<DocumentPageViewRecentActivity documentId={document.id} userId={user.id} />
|
<DocumentPageViewRecentActivity documentId={document.id} userId={user.id} />
|
||||||
@ -255,4 +280,4 @@ export const DocumentPageView = async ({ documentId, team }: DocumentPageViewPro
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
@ -1,37 +1,49 @@
|
|||||||
import { Plural, Trans } from '@lingui/macro';
|
import { Plural, Trans } from '@lingui/macro';
|
||||||
import type { Team } from '@prisma/client';
|
|
||||||
import { TeamMemberRole } from '@prisma/client';
|
import { TeamMemberRole } from '@prisma/client';
|
||||||
import { DocumentStatus as InternalDocumentStatus } from '@prisma/client';
|
import { DocumentStatus as InternalDocumentStatus } from '@prisma/client';
|
||||||
import { ChevronLeft, Users2 } from 'lucide-react';
|
import { ChevronLeft, Users2 } from 'lucide-react';
|
||||||
|
import { Link, redirect } from 'react-router';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
|
import { getRequiredSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||||
import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
|
import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
|
||||||
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
|
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
|
||||||
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||||
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
|
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
|
||||||
import { DocumentStatus } from '~/components/formatter/document-status';
|
import { DocumentStatus } from '~/components/formatter/document-status';
|
||||||
|
import { DocumentEditForm } from '~/components/pages/document/document-edit-form';
|
||||||
|
|
||||||
import { EditDocumentForm } from '../edit-document';
|
import type { Route } from './+types/$id.edit';
|
||||||
|
|
||||||
export type DocumentEditPageViewProps = {
|
export async function loader({ request, params }: Route.LoaderArgs) {
|
||||||
documentId: number;
|
const { id } = params;
|
||||||
team?: Team & { currentTeamMember: { role: TeamMemberRole } };
|
|
||||||
};
|
const { user } = await getRequiredSession(request);
|
||||||
|
|
||||||
|
// Todo: Get from parent loader, this is just for testing.
|
||||||
|
const team = await prisma.team.findFirst({
|
||||||
|
where: {
|
||||||
|
documents: {
|
||||||
|
some: {
|
||||||
|
id: Number(id),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const documentId = Number(id);
|
||||||
|
|
||||||
export const DocumentEditPageView = async ({ documentId, team }: DocumentEditPageViewProps) => {
|
|
||||||
const documentRootPath = formatDocumentsPath(team?.url);
|
const documentRootPath = formatDocumentsPath(team?.url);
|
||||||
|
|
||||||
if (!documentId || Number.isNaN(documentId)) {
|
if (!documentId || Number.isNaN(documentId)) {
|
||||||
redirect(documentRootPath);
|
return redirect(documentRootPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { user } = await getRequiredServerComponentSession();
|
|
||||||
|
|
||||||
const document = await getDocumentWithDetailsById({
|
const document = await getDocumentWithDetailsById({
|
||||||
documentId,
|
documentId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@ -39,7 +51,7 @@ export const DocumentEditPageView = async ({ documentId, team }: DocumentEditPag
|
|||||||
}).catch(() => null);
|
}).catch(() => null);
|
||||||
|
|
||||||
if (document?.teamId && !team?.url) {
|
if (document?.teamId && !team?.url) {
|
||||||
redirect(documentRootPath);
|
return redirect(documentRootPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
const documentVisibility = document?.visibility;
|
const documentVisibility = document?.visibility;
|
||||||
@ -59,15 +71,15 @@ export const DocumentEditPageView = async ({ documentId, team }: DocumentEditPag
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!document) {
|
if (!document) {
|
||||||
redirect(documentRootPath);
|
return redirect(documentRootPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (team && !canAccessDocument) {
|
if (team && !canAccessDocument) {
|
||||||
redirect(documentRootPath);
|
return redirect(documentRootPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.status === InternalDocumentStatus.COMPLETED) {
|
if (document.status === InternalDocumentStatus.COMPLETED) {
|
||||||
redirect(`${documentRootPath}/${documentId}`);
|
return redirect(`${documentRootPath}/${documentId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { documentMeta, recipients } = document;
|
const { documentMeta, recipients } = document;
|
||||||
@ -94,9 +106,21 @@ export const DocumentEditPageView = async ({ documentId, team }: DocumentEditPag
|
|||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
document,
|
||||||
|
documentRootPath,
|
||||||
|
isDocumentEnterprise,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DocumentEditPage({ loaderData }: Route.ComponentProps) {
|
||||||
|
const { document, documentRootPath, isDocumentEnterprise } = loaderData;
|
||||||
|
|
||||||
|
const { recipients } = document;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
|
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
|
||||||
<Link href={documentRootPath} className="flex items-center text-[#7AC455] hover:opacity-80">
|
<Link to={documentRootPath} className="flex items-center text-[#7AC455] hover:opacity-80">
|
||||||
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
||||||
<Trans>Documents</Trans>
|
<Trans>Documents</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
@ -128,7 +152,7 @@ export const DocumentEditPageView = async ({ documentId, team }: DocumentEditPag
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EditDocumentForm
|
<DocumentEditForm
|
||||||
className="mt-6"
|
className="mt-6"
|
||||||
initialDocument={document}
|
initialDocument={document}
|
||||||
documentRootPath={documentRootPath}
|
documentRootPath={documentRootPath}
|
||||||
@ -136,4 +160,4 @@ export const DocumentEditPageView = async ({ documentId, team }: DocumentEditPag
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
@ -4,36 +4,50 @@ import { useLingui } from '@lingui/react';
|
|||||||
import type { Recipient } from '@prisma/client';
|
import type { Recipient } from '@prisma/client';
|
||||||
import { ChevronLeft } from 'lucide-react';
|
import { ChevronLeft } from 'lucide-react';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import { Link, redirect } from 'react-router';
|
||||||
|
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
import { getRequiredSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||||
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
import { Card } from '@documenso/ui/primitives/card';
|
import { Card } from '@documenso/ui/primitives/card';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DocumentStatus as DocumentStatusComponent,
|
DocumentStatus as DocumentStatusComponent,
|
||||||
FRIENDLY_STATUS_MAP,
|
FRIENDLY_STATUS_MAP,
|
||||||
} from '~/components/formatter/document-status';
|
} from '~/components/formatter/document-status';
|
||||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
import { DocumentAuditLogDownloadButton } from '~/components/pages/document/document-audit-log-download-button';
|
||||||
|
import { DocumentCertificateDownloadButton } from '~/components/pages/document/document-certificate-download-button';
|
||||||
|
import { DocumentLogsTable } from '~/components/tables/document-logs-table';
|
||||||
|
|
||||||
import { DocumentLogsDataTable } from './document-logs-data-table';
|
import type { Route } from './+types/$id.logs';
|
||||||
import { DownloadAuditLogButton } from './download-audit-log-button';
|
|
||||||
import { DownloadCertificateButton } from './download-certificate-button';
|
|
||||||
|
|
||||||
export type DocumentLogsPageViewProps = {
|
export async function loader({ request, params }: Route.LoaderArgs) {
|
||||||
documentId: number;
|
const { id } = params;
|
||||||
};
|
|
||||||
|
|
||||||
export const DocumentLogsPageView = async ({ documentId }: DocumentLogsPageViewProps) => {
|
const { user } = await getRequiredSession(request);
|
||||||
const { _, i18n } = useLingui();
|
|
||||||
|
|
||||||
const team = useOptionalCurrentTeam();
|
// Todo: Get from parent loader, this is just for testing.
|
||||||
|
const team = await prisma.team.findFirst({
|
||||||
|
where: {
|
||||||
|
documents: {
|
||||||
|
some: {
|
||||||
|
id: Number(id),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const documentId = Number(id);
|
||||||
|
|
||||||
const documentRootPath = formatDocumentsPath(team?.url);
|
const documentRootPath = formatDocumentsPath(team?.url);
|
||||||
|
|
||||||
const { user } = await getRequiredServerComponentSession();
|
if (!documentId || Number.isNaN(documentId)) {
|
||||||
|
return redirect(documentRootPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Todo: Get detailed?
|
||||||
const [document, recipients] = await Promise.all([
|
const [document, recipients] = await Promise.all([
|
||||||
getDocumentById({
|
getDocumentById({
|
||||||
documentId,
|
documentId,
|
||||||
@ -48,9 +62,21 @@ export const DocumentLogsPageView = async ({ documentId }: DocumentLogsPageViewP
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (!document || !document.documentData) {
|
if (!document || !document.documentData) {
|
||||||
redirect(documentRootPath);
|
return redirect(documentRootPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
document,
|
||||||
|
documentRootPath,
|
||||||
|
recipients,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps) {
|
||||||
|
const { document, documentRootPath, recipients } = loaderData;
|
||||||
|
|
||||||
|
const { _, i18n } = useLingui();
|
||||||
|
|
||||||
const documentInformation: { description: MessageDescriptor; value: string }[] = [
|
const documentInformation: { description: MessageDescriptor; value: string }[] = [
|
||||||
{
|
{
|
||||||
description: msg`Document title`,
|
description: msg`Document title`,
|
||||||
@ -97,11 +123,10 @@ export const DocumentLogsPageView = async ({ documentId }: DocumentLogsPageViewP
|
|||||||
|
|
||||||
return `[${recipient.role}] ${text}`;
|
return `[${recipient.role}] ${text}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
|
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
|
||||||
<Link
|
<Link
|
||||||
href={`${documentRootPath}/${document.id}`}
|
to={`${documentRootPath}/${document.id}`}
|
||||||
className="flex items-center text-[#7AC455] hover:opacity-80"
|
className="flex items-center text-[#7AC455] hover:opacity-80"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
||||||
@ -127,14 +152,13 @@ export const DocumentLogsPageView = async ({ documentId }: DocumentLogsPageViewP
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 flex w-full flex-row sm:mt-0 sm:w-auto sm:self-end">
|
<div className="mt-4 flex w-full flex-row sm:mt-0 sm:w-auto sm:self-end">
|
||||||
<DownloadCertificateButton
|
<DocumentCertificateDownloadButton
|
||||||
className="mr-2"
|
className="mr-2"
|
||||||
documentId={document.id}
|
documentId={document.id}
|
||||||
documentStatus={document.status}
|
documentStatus={document.status}
|
||||||
teamId={team?.id}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DownloadAuditLogButton teamId={team?.id} documentId={document.id} />
|
<DocumentAuditLogDownloadButton documentId={document.id} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -161,8 +185,8 @@ export const DocumentLogsPageView = async ({ documentId }: DocumentLogsPageViewP
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="mt-6">
|
<section className="mt-6">
|
||||||
<DocumentLogsDataTable documentId={document.id} />
|
<DocumentLogsTable documentId={document.id} />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
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 { useLingui } from '@lingui/react';
|
||||||
|
|
||||||
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
||||||
|
import { AccountDeleteDialog } from '~/components/dialogs/account-delete-dialog';
|
||||||
import { AvatarImageForm } from '~/components/forms/avatar-image';
|
import { AvatarImageForm } from '~/components/forms/avatar-image';
|
||||||
import { ProfileForm } from '~/components/forms/profile';
|
import { ProfileForm } from '~/components/forms/profile';
|
||||||
|
|
||||||
import type { Route } from './+types/profile';
|
export function meta() {
|
||||||
|
|
||||||
// import { DeleteAccountDialog } from './settings/profile/delete-account-dialog';
|
|
||||||
|
|
||||||
export function meta(_args: Route.MetaArgs) {
|
|
||||||
return [{ title: 'Profile' }];
|
return [{ title: 'Profile' }];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,7 +25,7 @@ export default function SettingsProfile() {
|
|||||||
|
|
||||||
<hr className="my-4 max-w-xl" />
|
<hr className="my-4 max-w-xl" />
|
||||||
|
|
||||||
{/* <DeleteAccountDialog className="max-w-xl" /> */}
|
<AccountDeleteDialog className="max-w-xl" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import { trpc } from '@documenso/trpc/react';
|
|||||||
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
|
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
|
||||||
|
|
||||||
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
||||||
import { CreateTeamDialog } from '~/components/(teams)/dialogs/create-team-dialog';
|
|
||||||
import { UserSettingsTeamsPageDataTable } from '~/components/(teams)/tables/user-settings-teams-page-data-table';
|
import { UserSettingsTeamsPageDataTable } from '~/components/(teams)/tables/user-settings-teams-page-data-table';
|
||||||
|
import { TeamCreateDialog } from '~/components/dialogs/team-create-dialog';
|
||||||
|
|
||||||
import { TeamEmailUsage } from './team-email-usage';
|
import { TeamEmailUsage } from './team-email-usage';
|
||||||
import { TeamInvitations } from './team-invitations';
|
import { TeamInvitations } from './team-invitations';
|
||||||
@ -23,7 +23,7 @@ export default function TeamsSettingsPage() {
|
|||||||
title={_(msg`Teams`)}
|
title={_(msg`Teams`)}
|
||||||
subtitle={_(msg`Manage all teams you are currently associated with.`)}
|
subtitle={_(msg`Manage all teams you are currently associated with.`)}
|
||||||
>
|
>
|
||||||
<CreateTeamDialog />
|
<TeamCreateDialog />
|
||||||
</SettingsHeader>
|
</SettingsHeader>
|
||||||
|
|
||||||
<UserSettingsTeamsPageDataTable />
|
<UserSettingsTeamsPageDataTable />
|
||||||
|
|||||||
@ -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 { Trans } from '@lingui/macro';
|
||||||
import { Link, redirect } from 'react-router';
|
import { Link, redirect } from 'react-router';
|
||||||
|
|
||||||
|
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||||
import {
|
import {
|
||||||
IS_GOOGLE_SSO_ENABLED,
|
IS_GOOGLE_SSO_ENABLED,
|
||||||
IS_OIDC_SSO_ENABLED,
|
IS_OIDC_SSO_ENABLED,
|
||||||
OIDC_PROVIDER_LABEL,
|
OIDC_PROVIDER_LABEL,
|
||||||
} from '@documenso/lib/constants/auth';
|
} from '@documenso/lib/constants/auth';
|
||||||
|
import { env } from '@documenso/lib/utils/env';
|
||||||
|
|
||||||
import { SignInForm } from '~/components/forms/signin';
|
import { SignInForm } from '~/components/forms/signin';
|
||||||
|
|
||||||
import type { Route } from './+types/signin';
|
import type { Route } from './+types/signin';
|
||||||
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
|
||||||
|
|
||||||
export function meta(_args: Route.MetaArgs) {
|
export function meta(_args: Route.MetaArgs) {
|
||||||
return [{ title: 'Sign In' }];
|
return [{ title: 'Sign In' }];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loader({ request }: Route.LoaderArgs) {
|
export async function loader({ request }: Route.LoaderArgs) {
|
||||||
const session = await getSession(request)
|
const session = await getSession(request);
|
||||||
|
|
||||||
if (session.isAuthenticated) {
|
if (session.isAuthenticated) {
|
||||||
return redirect('/documents');
|
return redirect('/documents');
|
||||||
@ -25,9 +26,7 @@ export async function loader({ request }: Route.LoaderArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function SignIn() {
|
export default function SignIn() {
|
||||||
// Todo
|
const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
|
||||||
// const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
|
|
||||||
const NEXT_PUBLIC_DISABLE_SIGNUP = 'false';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-screen max-w-lg px-4">
|
<div className="w-screen max-w-lg px-4">
|
||||||
|
|||||||
@ -1,19 +1,16 @@
|
|||||||
import { redirect } from 'react-router';
|
import { redirect } from 'react-router';
|
||||||
|
|
||||||
import { IS_GOOGLE_SSO_ENABLED, IS_OIDC_SSO_ENABLED } from '@documenso/lib/constants/auth';
|
import { IS_GOOGLE_SSO_ENABLED, IS_OIDC_SSO_ENABLED } from '@documenso/lib/constants/auth';
|
||||||
|
import { env } from '@documenso/lib/utils/env';
|
||||||
|
|
||||||
import { SignUpForm } from '~/components/forms/signup';
|
import { SignUpForm } from '~/components/forms/signup';
|
||||||
|
|
||||||
import type { Route } from './+types/_unauth.signup';
|
export function meta() {
|
||||||
|
|
||||||
export function meta(_args: Route.MetaArgs) {
|
|
||||||
return [{ title: 'Sign Up' }];
|
return [{ title: 'Sign Up' }];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loader() {
|
export function loader() {
|
||||||
// Todo
|
const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
|
||||||
// const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
|
|
||||||
const NEXT_PUBLIC_DISABLE_SIGNUP: string = 'false';
|
|
||||||
|
|
||||||
if (NEXT_PUBLIC_DISABLE_SIGNUP === 'true') {
|
if (NEXT_PUBLIC_DISABLE_SIGNUP === 'true') {
|
||||||
return redirect('/signin');
|
return redirect('/signin');
|
||||||
|
|||||||
@ -7,7 +7,8 @@
|
|||||||
"dev": "react-router dev",
|
"dev": "react-router dev",
|
||||||
"start": "cross-env NODE_ENV=production node dist/server/index.js",
|
"start": "cross-env NODE_ENV=production node dist/server/index.js",
|
||||||
"clean": "rimraf .react-router && rimraf node_modules",
|
"clean": "rimraf .react-router && rimraf node_modules",
|
||||||
"typecheck": "react-router typegen && tsc"
|
"typecheck": "react-router typegen && tsc",
|
||||||
|
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@epic-web/remember": "^1.1.0",
|
"@epic-web/remember": "^1.1.0",
|
||||||
|
|||||||
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 serverAdapter from 'hono-react-router-adapter/vite';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import tailwindcss from 'tailwindcss';
|
import tailwindcss from 'tailwindcss';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig, loadEnv } from 'vite';
|
||||||
import macrosPlugin from 'vite-plugin-babel-macros';
|
import macrosPlugin from 'vite-plugin-babel-macros';
|
||||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
envDir: path.join(__dirname, '../../'),
|
envDir: path.join(__dirname, '../../'),
|
||||||
envPrefix: 'NEXT_',
|
envPrefix: '__DO_NOT_USE_OR_YOU_WILL_BE_FIRED__',
|
||||||
define: {
|
define: {
|
||||||
'process.env': {},
|
'process.env': {
|
||||||
|
...process.env,
|
||||||
|
...loadEnv('development', path.join(__dirname, '../../'), ''),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
css: {
|
css: {
|
||||||
postcss: {
|
postcss: {
|
||||||
@ -20,6 +23,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
ssr: {
|
ssr: {
|
||||||
|
// , 'next/font/google' doesnot work
|
||||||
noExternal: ['react-dropzone', 'recharts'],
|
noExternal: ['react-dropzone', 'recharts'],
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
@ -36,7 +40,7 @@ export default defineConfig({
|
|||||||
tsconfigPaths(),
|
tsconfigPaths(),
|
||||||
],
|
],
|
||||||
// optimizeDeps: {
|
// optimizeDeps: {
|
||||||
// exclude: ['@node-rs/bcrypt', '@node-rs/bcrypt-wasm32-wasi', 'react-dropzone', '@documenso/ui'], // Todo: Probably remove.
|
// exclude: ['next/font/google'], // Todo: Probably remove.
|
||||||
// force: true,
|
// force: true,
|
||||||
// },
|
// },
|
||||||
});
|
});
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { downloadFile } from '@documenso/lib/client-only/download-file';
|
import { downloadFile } from '@documenso/lib/client-only/download-file';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -147,7 +146,7 @@ export const ViewRecoveryCodesDialog = () => {
|
|||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{match(AppError.parseError(error).message)
|
{match(AppError.parseError(error).message)
|
||||||
.with(ErrorCode.INCORRECT_TWO_FACTOR_CODE, () => (
|
.with('INCORRECT_TWO_FACTOR_CODE', () => (
|
||||||
<Trans>Invalid code. Please try again.</Trans>
|
<Trans>Invalid code. Please try again.</Trans>
|
||||||
))
|
))
|
||||||
.otherwise(() => (
|
.otherwise(() => (
|
||||||
|
|||||||
@ -76,10 +76,10 @@ export const ZSignUpFormV2Schema = z
|
|||||||
|
|
||||||
export const signupErrorMessages: Record<string, MessageDescriptor> = {
|
export const signupErrorMessages: Record<string, MessageDescriptor> = {
|
||||||
SIGNUP_DISABLED: msg`Signups are disabled.`,
|
SIGNUP_DISABLED: msg`Signups are disabled.`,
|
||||||
|
PROFILE_URL_TAKEN: msg`This username has already been taken`,
|
||||||
|
PREMIUM_PROFILE_URL: msg`Only subscribers can have a username shorter than 6 characters`,
|
||||||
[AppErrorCode.ALREADY_EXISTS]: msg`User with this email already exists. Please use a different email address.`,
|
[AppErrorCode.ALREADY_EXISTS]: msg`User with this email already exists. Please use a different email address.`,
|
||||||
[AppErrorCode.INVALID_REQUEST]: msg`We were unable to create your account. Please review the information you provided and try again.`,
|
[AppErrorCode.INVALID_REQUEST]: msg`We were unable to create your account. Please review the information you provided and try again.`,
|
||||||
[AppErrorCode.PROFILE_URL_TAKEN]: msg`This username has already been taken`,
|
|
||||||
[AppErrorCode.PREMIUM_PROFILE_URL]: msg`Only subscribers can have a username shorter than 6 characters`,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSignUpFormV2Schema = z.infer<typeof ZSignUpFormV2Schema>;
|
export type TSignUpFormV2Schema = z.infer<typeof ZSignUpFormV2Schema>;
|
||||||
|
|||||||
@ -7,8 +7,8 @@ import type { AuthAppType } from '../server';
|
|||||||
import type {
|
import type {
|
||||||
TForgotPasswordSchema,
|
TForgotPasswordSchema,
|
||||||
TResetPasswordSchema,
|
TResetPasswordSchema,
|
||||||
TSignInFormSchema,
|
TSignInSchema,
|
||||||
TSignUpRequestSchema,
|
TSignUpSchema,
|
||||||
TVerifyEmailSchema,
|
TVerifyEmailSchema,
|
||||||
} from '../server/types/email-password';
|
} from '../server/types/email-password';
|
||||||
import type { TPasskeyAuthorizeSchema } from '../server/types/passkey';
|
import type { TPasskeyAuthorizeSchema } from '../server/types/passkey';
|
||||||
@ -32,18 +32,6 @@ export class AuthClient {
|
|||||||
return this.client.session.$get();
|
return this.client.session.$get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public passkey = {
|
|
||||||
signIn: async (data: TPasskeyAuthorizeSchema) => {
|
|
||||||
const result = await this.client['passkey'].authorize.$post({ json: data });
|
|
||||||
|
|
||||||
if (result.ok) {
|
|
||||||
return result.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(result.statusText);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
private async handleResponse<T>(response: ClientResponse<T>) {
|
private async handleResponse<T>(response: ClientResponse<T>) {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
@ -59,9 +47,16 @@ export class AuthClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public emailPassword = {
|
public emailPassword = {
|
||||||
signIn: async (data: TSignInFormSchema) => {
|
signIn: async (data: TSignInSchema & { redirectUrl?: string }) => {
|
||||||
const response = await this.client['email-password'].authorize.$post({ json: data });
|
const response = await this.client['email-password'].authorize
|
||||||
return this.handleResponse(response);
|
.$post({ json: data })
|
||||||
|
.then(this.handleResponse);
|
||||||
|
|
||||||
|
if (data.redirectUrl) {
|
||||||
|
window.location.href = data.redirectUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
forgotPassword: async (data: TForgotPasswordSchema) => {
|
forgotPassword: async (data: TForgotPasswordSchema) => {
|
||||||
@ -74,7 +69,7 @@ export class AuthClient {
|
|||||||
return this.handleResponse(response);
|
return this.handleResponse(response);
|
||||||
},
|
},
|
||||||
|
|
||||||
signUp: async (data: TSignUpRequestSchema) => {
|
signUp: async (data: TSignUpSchema) => {
|
||||||
const response = await this.client['email-password']['signup'].$post({ json: data });
|
const response = await this.client['email-password']['signup'].$post({ json: data });
|
||||||
return this.handleResponse(response);
|
return this.handleResponse(response);
|
||||||
},
|
},
|
||||||
@ -85,20 +80,29 @@ export class AuthClient {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
public google = {
|
public passkey = {
|
||||||
signIn: async () => {
|
signIn: async (data: TPasskeyAuthorizeSchema & { redirectUrl?: string }) => {
|
||||||
const response = await this.client['google'].authorize.$post();
|
const response = await this.client['passkey'].authorize
|
||||||
|
.$post({ json: data })
|
||||||
|
.then(this.handleResponse);
|
||||||
|
|
||||||
// const parsedResponse = this.handleResponse(response);
|
if (data.redirectUrl) {
|
||||||
if (!response.ok) {
|
window.location.href = data.redirectUrl;
|
||||||
const error = await response.json();
|
|
||||||
|
|
||||||
throw AppError.parseError(error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const test = await response.json();
|
return response;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
window.location.href = test.redirectUrl;
|
public google = {
|
||||||
|
signIn: async () => {
|
||||||
|
const response = await this.client['google'].authorize.$post().then(this.handleResponse);
|
||||||
|
|
||||||
|
if (response.redirectUrl) {
|
||||||
|
window.location.href = response.redirectUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
|
import { env } from '@documenso/lib/utils/env';
|
||||||
|
|
||||||
|
// Todo: Delete
|
||||||
export const authDebugger = (message: string) => {
|
export const authDebugger = (message: string) => {
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (env('NODE_ENV') === 'development') {
|
||||||
console.log(`[DEBUG]: ${message}`);
|
console.log(`[DEBUG]: ${message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { Hono } from 'hono';
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { jobsClient } from '@documenso/lib/jobs/client';
|
import { jobsClient } from '@documenso/lib/jobs/client';
|
||||||
import { disableTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/disable-2fa';
|
import { disableTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/disable-2fa';
|
||||||
@ -16,19 +17,25 @@ import { createUser } from '@documenso/lib/server-only/user/create-user';
|
|||||||
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
|
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
|
||||||
import { getMostRecentVerificationTokenByUserId } from '@documenso/lib/server-only/user/get-most-recent-verification-token-by-user-id';
|
import { getMostRecentVerificationTokenByUserId } from '@documenso/lib/server-only/user/get-most-recent-verification-token-by-user-id';
|
||||||
import { resetPassword } from '@documenso/lib/server-only/user/reset-password';
|
import { resetPassword } from '@documenso/lib/server-only/user/reset-password';
|
||||||
import { verifyEmail } from '@documenso/lib/server-only/user/verify-email';
|
import { updatePassword } from '@documenso/lib/server-only/user/update-password';
|
||||||
|
import {
|
||||||
|
EMAIL_VERIFICATION_STATE,
|
||||||
|
verifyEmail,
|
||||||
|
} from '@documenso/lib/server-only/user/verify-email';
|
||||||
|
import { env } from '@documenso/lib/utils/env';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
|
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { AuthenticationErrorCode } from '../lib/errors/error-codes';
|
import { AuthenticationErrorCode } from '../lib/errors/error-codes';
|
||||||
import { onAuthorize } from '../lib/utils/authorizer';
|
import { onAuthorize } from '../lib/utils/authorizer';
|
||||||
import { getRequiredSession } from '../lib/utils/get-session';
|
import { getRequiredSession, getSession } from '../lib/utils/get-session';
|
||||||
import type { HonoAuthContext } from '../types/context';
|
import type { HonoAuthContext } from '../types/context';
|
||||||
import {
|
import {
|
||||||
ZForgotPasswordSchema,
|
ZForgotPasswordSchema,
|
||||||
ZResetPasswordSchema,
|
ZResetPasswordSchema,
|
||||||
ZSignInFormSchema,
|
ZSignInSchema,
|
||||||
ZSignUpRequestSchema,
|
ZSignUpSchema,
|
||||||
|
ZUpdatePasswordSchema,
|
||||||
ZVerifyEmailSchema,
|
ZVerifyEmailSchema,
|
||||||
} from '../types/email-password';
|
} from '../types/email-password';
|
||||||
|
|
||||||
@ -36,7 +43,7 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
|
|||||||
/**
|
/**
|
||||||
* Authorize endpoint.
|
* Authorize endpoint.
|
||||||
*/
|
*/
|
||||||
.post('/authorize', zValidator('json', ZSignInFormSchema), async (c) => {
|
.post('/authorize', zValidator('json', ZSignInSchema), async (c) => {
|
||||||
const requestMetadata = c.get('requestMetadata');
|
const requestMetadata = c.get('requestMetadata');
|
||||||
|
|
||||||
const { email, password, totpCode, backupCode } = c.req.valid('json');
|
const { email, password, totpCode, backupCode } = c.req.valid('json');
|
||||||
@ -48,8 +55,8 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!user || !user.password) {
|
if (!user || !user.password) {
|
||||||
throw new AppError(AuthenticationErrorCode.NotFound, {
|
throw new AppError(AuthenticationErrorCode.InvalidCredentials, {
|
||||||
message: 'User not found',
|
message: 'Invalid email or password',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +92,7 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
throw new AppError(AuthenticationErrorCode.IncorrectTwoFactorCode);
|
throw new AppError(AuthenticationErrorCode.InvalidTwoFactorCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,20 +132,20 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
|
|||||||
/**
|
/**
|
||||||
* Signup endpoint.
|
* Signup endpoint.
|
||||||
*/
|
*/
|
||||||
.post('/signup', zValidator('json', ZSignUpRequestSchema), async (c) => {
|
.post('/signup', zValidator('json', ZSignUpSchema), async (c) => {
|
||||||
// if (NEXT_PUBLIC_DISABLE_SIGNUP() === 'true') {
|
if (env('NEXT_PUBLIC_DISABLE_SIGNUP') === 'true') {
|
||||||
// throw new AppError('SIGNUP_DISABLED', {
|
throw new AppError('SIGNUP_DISABLED', {
|
||||||
// message: 'Signups are disabled.',
|
message: 'Signups are disabled.',
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
|
|
||||||
const { name, email, password, signature, url } = c.req.valid('json');
|
const { name, email, password, signature, url } = c.req.valid('json');
|
||||||
|
|
||||||
// if (IS_BILLING_ENABLED() && url && url.length < 6) {
|
if (IS_BILLING_ENABLED() && url && url.length < 6) {
|
||||||
// throw new AppError(AppErrorCode.PREMIUM_PROFILE_URL, {
|
throw new AppError('PREMIUM_PROFILE_URL', {
|
||||||
// message: 'Only subscribers can have a username shorter than 6 characters',
|
message: 'Only subscribers can have a username shorter than 6 characters',
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
|
|
||||||
const user = await createUser({ name, email, password, signature, url });
|
const user = await createUser({ name, email, password, signature, url });
|
||||||
|
|
||||||
@ -149,18 +156,59 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Todo: Check this.
|
return c.text('OK', 201);
|
||||||
return c.json({
|
})
|
||||||
user,
|
/**
|
||||||
|
* Update password endpoint.
|
||||||
|
*/
|
||||||
|
.post('/update-password', zValidator('json', ZUpdatePasswordSchema), async (c) => {
|
||||||
|
const { password, currentPassword } = c.req.valid('json');
|
||||||
|
const requestMetadata = c.get('requestMetadata');
|
||||||
|
|
||||||
|
const session = await getSession(c);
|
||||||
|
|
||||||
|
if (!session.isAuthenticated) {
|
||||||
|
throw new AppError(AuthenticationErrorCode.Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
await updatePassword({
|
||||||
|
userId: session.user.id,
|
||||||
|
password,
|
||||||
|
currentPassword,
|
||||||
|
requestMetadata,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return c.text('OK', 201);
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* Verify email endpoint.
|
* Verify email endpoint.
|
||||||
*/
|
*/
|
||||||
.post('/verify-email', zValidator('json', ZVerifyEmailSchema), async (c) => {
|
.post('/verify-email', zValidator('json', ZVerifyEmailSchema), async (c) => {
|
||||||
await verifyEmail({ token: c.req.valid('json').token });
|
const { state, userId } = await verifyEmail({ token: c.req.valid('json').token });
|
||||||
|
|
||||||
return c.text('OK', 201);
|
// If email is verified, automatically authenticate user.
|
||||||
|
if (state === EMAIL_VERIFICATION_STATE.VERIFIED && userId !== null) {
|
||||||
|
await onAuthorize({ userId }, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
state,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Resend verification email endpoint.
|
||||||
|
*/
|
||||||
|
.post('/resend-email', zValidator('json', ZVerifyEmailSchema), async (c) => {
|
||||||
|
const { state, userId } = await verifyEmail({ token: c.req.valid('json').token });
|
||||||
|
|
||||||
|
// If email is verified, automatically authenticate user.
|
||||||
|
if (state === EMAIL_VERIFICATION_STATE.VERIFIED && userId !== null) {
|
||||||
|
await onAuthorize({ userId }, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
state,
|
||||||
|
});
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* Forgot password endpoint.
|
* Forgot password endpoint.
|
||||||
@ -180,9 +228,12 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
|
|||||||
.post('/reset-password', zValidator('json', ZResetPasswordSchema), async (c) => {
|
.post('/reset-password', zValidator('json', ZResetPasswordSchema), async (c) => {
|
||||||
const { token, password } = c.req.valid('json');
|
const { token, password } = c.req.valid('json');
|
||||||
|
|
||||||
|
const requestMetadata = c.get('requestMetadata');
|
||||||
|
|
||||||
await resetPassword({
|
await resetPassword({
|
||||||
token,
|
token,
|
||||||
password,
|
password,
|
||||||
|
requestMetadata,
|
||||||
});
|
});
|
||||||
|
|
||||||
return c.text('OK', 201);
|
return c.text('OK', 201);
|
||||||
@ -291,9 +342,7 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
|
|||||||
requestMetadata,
|
requestMetadata,
|
||||||
});
|
});
|
||||||
|
|
||||||
return c.json({
|
return c.text('OK', 201);
|
||||||
success: true,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { getCookie, setCookie } from 'hono/cookie';
|
|||||||
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { setupTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/setup-2fa';
|
import { setupTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/setup-2fa';
|
||||||
|
import { env } from '@documenso/lib/utils/env';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { AuthenticationErrorCode } from '../lib/errors/error-codes';
|
import { AuthenticationErrorCode } from '../lib/errors/error-codes';
|
||||||
@ -12,8 +13,8 @@ import { getRequiredSession } from '../lib/utils/get-session';
|
|||||||
import type { HonoAuthContext } from '../types/context';
|
import type { HonoAuthContext } from '../types/context';
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
clientId: import.meta.env.NEXT_PRIVATE_GOOGLE_CLIENT_ID,
|
clientId: env('NEXT_PRIVATE_GOOGLE_CLIENT_ID'),
|
||||||
clientSecret: import.meta.env.NEXT_PRIVATE_GOOGLE_CLIENT_SECRET,
|
clientSecret: env('NEXT_PRIVATE_GOOGLE_CLIENT_SECRET'),
|
||||||
redirectUri: 'http://localhost:3000/api/auth/google/callback',
|
redirectUri: 'http://localhost:3000/api/auth/google/callback',
|
||||||
scope: ['openid', 'email', 'profile'],
|
scope: ['openid', 'email', 'profile'],
|
||||||
id: 'google',
|
id: 'google',
|
||||||
@ -36,7 +37,7 @@ export const googleRoute = new Hono<HonoAuthContext>()
|
|||||||
setCookie(c, 'google_oauth_state', state, {
|
setCookie(c, 'google_oauth_state', state, {
|
||||||
path: '/',
|
path: '/',
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: env('NODE_ENV') === 'production',
|
||||||
maxAge: 60 * 10, // 10 minutes
|
maxAge: 60 * 10, // 10 minutes
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
});
|
});
|
||||||
@ -44,7 +45,8 @@ export const googleRoute = new Hono<HonoAuthContext>()
|
|||||||
setCookie(c, 'google_code_verifier', codeVerifier, {
|
setCookie(c, 'google_code_verifier', codeVerifier, {
|
||||||
path: '/',
|
path: '/',
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
// Todo: Might not be node_env but something vite specific?
|
||||||
|
secure: env('NODE_ENV') === 'production',
|
||||||
maxAge: 60 * 10, // 10 minutes
|
maxAge: 60 * 10, // 10 minutes
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,14 +5,14 @@ export const ZCurrentPasswordSchema = z
|
|||||||
.min(6, { message: 'Must be at least 6 characters in length' })
|
.min(6, { message: 'Must be at least 6 characters in length' })
|
||||||
.max(72);
|
.max(72);
|
||||||
|
|
||||||
export const ZSignInFormSchema = z.object({
|
export const ZSignInSchema = z.object({
|
||||||
email: z.string().email().min(1),
|
email: z.string().email().min(1),
|
||||||
password: ZCurrentPasswordSchema,
|
password: ZCurrentPasswordSchema,
|
||||||
totpCode: z.string().trim().optional(),
|
totpCode: z.string().trim().optional(),
|
||||||
backupCode: z.string().trim().optional(),
|
backupCode: z.string().trim().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSignInFormSchema = z.infer<typeof ZSignInFormSchema>;
|
export type TSignInSchema = z.infer<typeof ZSignInSchema>;
|
||||||
|
|
||||||
export const ZPasswordSchema = z
|
export const ZPasswordSchema = z
|
||||||
.string()
|
.string()
|
||||||
@ -31,7 +31,7 @@ export const ZPasswordSchema = z
|
|||||||
message: 'One special character is required',
|
message: 'One special character is required',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZSignUpRequestSchema = z.object({
|
export const ZSignUpSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
password: ZPasswordSchema,
|
password: ZPasswordSchema,
|
||||||
@ -47,7 +47,7 @@ export const ZSignUpRequestSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSignUpRequestSchema = z.infer<typeof ZSignUpRequestSchema>;
|
export type TSignUpSchema = z.infer<typeof ZSignUpSchema>;
|
||||||
|
|
||||||
export const ZForgotPasswordSchema = z.object({
|
export const ZForgotPasswordSchema = z.object({
|
||||||
email: z.string().email().min(1),
|
email: z.string().email().min(1),
|
||||||
@ -67,3 +67,10 @@ export const ZVerifyEmailSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type TVerifyEmailSchema = z.infer<typeof ZVerifyEmailSchema>;
|
export type TVerifyEmailSchema = z.infer<typeof ZVerifyEmailSchema>;
|
||||||
|
|
||||||
|
export const ZUpdatePasswordSchema = z.object({
|
||||||
|
currentPassword: ZCurrentPasswordSchema,
|
||||||
|
password: ZPasswordSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TUpdatePasswordSchema = z.infer<typeof ZUpdatePasswordSchema>;
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import type { Stripe } from '@documenso/lib/server-only/stripe';
|
|||||||
import { stripe } from '@documenso/lib/server-only/stripe';
|
import { stripe } from '@documenso/lib/server-only/stripe';
|
||||||
import { createTeamFromPendingTeam } from '@documenso/lib/server-only/team/create-team';
|
import { createTeamFromPendingTeam } from '@documenso/lib/server-only/team/create-team';
|
||||||
import { getFlag } from '@documenso/lib/universal/get-feature-flag';
|
import { getFlag } from '@documenso/lib/universal/get-feature-flag';
|
||||||
|
import { env } from '@documenso/lib/utils/env';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { onSubscriptionDeleted } from './on-subscription-deleted';
|
import { onSubscriptionDeleted } from './on-subscription-deleted';
|
||||||
@ -47,7 +48,7 @@ export const stripeWebhookHandler = async (
|
|||||||
const event = stripe.webhooks.constructEvent(
|
const event = stripe.webhooks.constructEvent(
|
||||||
body,
|
body,
|
||||||
signature,
|
signature,
|
||||||
process.env.NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET,
|
env('NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET'), // Todo: Test
|
||||||
);
|
);
|
||||||
|
|
||||||
await match(event.type)
|
await match(event.type)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type { Transporter } from 'nodemailer';
|
import type { Transporter } from 'nodemailer';
|
||||||
import { createTransport } from 'nodemailer';
|
import { createTransport } from 'nodemailer';
|
||||||
|
|
||||||
|
import { env } from '@documenso/lib/utils/env';
|
||||||
import { ResendTransport } from '@documenso/nodemailer-resend';
|
import { ResendTransport } from '@documenso/nodemailer-resend';
|
||||||
|
|
||||||
import { MailChannelsTransport } from './transports/mailchannels';
|
import { MailChannelsTransport } from './transports/mailchannels';
|
||||||
@ -51,13 +52,13 @@ import { MailChannelsTransport } from './transports/mailchannels';
|
|||||||
* - `NEXT_PRIVATE_SMTP_SERVICE` is optional and used specifically for well-known services like Gmail.
|
* - `NEXT_PRIVATE_SMTP_SERVICE` is optional and used specifically for well-known services like Gmail.
|
||||||
*/
|
*/
|
||||||
const getTransport = (): Transporter => {
|
const getTransport = (): Transporter => {
|
||||||
const transport = process.env.NEXT_PRIVATE_SMTP_TRANSPORT ?? 'smtp-auth';
|
const transport = env('NEXT_PRIVATE_SMTP_TRANSPORT') ?? 'smtp-auth';
|
||||||
|
|
||||||
if (transport === 'mailchannels') {
|
if (transport === 'mailchannels') {
|
||||||
return createTransport(
|
return createTransport(
|
||||||
MailChannelsTransport.makeTransport({
|
MailChannelsTransport.makeTransport({
|
||||||
apiKey: process.env.NEXT_PRIVATE_MAILCHANNELS_API_KEY,
|
apiKey: env('NEXT_PRIVATE_MAILCHANNELS_API_KEY'),
|
||||||
endpoint: process.env.NEXT_PRIVATE_MAILCHANNELS_ENDPOINT,
|
endpoint: env('NEXT_PRIVATE_MAILCHANNELS_ENDPOINT'),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -65,43 +66,41 @@ const getTransport = (): Transporter => {
|
|||||||
if (transport === 'resend') {
|
if (transport === 'resend') {
|
||||||
return createTransport(
|
return createTransport(
|
||||||
ResendTransport.makeTransport({
|
ResendTransport.makeTransport({
|
||||||
apiKey: process.env.NEXT_PRIVATE_RESEND_API_KEY || '',
|
apiKey: env('NEXT_PRIVATE_RESEND_API_KEY') || '',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transport === 'smtp-api') {
|
if (transport === 'smtp-api') {
|
||||||
if (!process.env.NEXT_PRIVATE_SMTP_HOST || !process.env.NEXT_PRIVATE_SMTP_APIKEY) {
|
if (!env('NEXT_PRIVATE_SMTP_HOST') || !env('NEXT_PRIVATE_SMTP_APIKEY')) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'SMTP API transport requires NEXT_PRIVATE_SMTP_HOST and NEXT_PRIVATE_SMTP_APIKEY',
|
'SMTP API transport requires NEXT_PRIVATE_SMTP_HOST and NEXT_PRIVATE_SMTP_APIKEY',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return createTransport({
|
return createTransport({
|
||||||
host: process.env.NEXT_PRIVATE_SMTP_HOST,
|
host: env('NEXT_PRIVATE_SMTP_HOST'),
|
||||||
port: Number(process.env.NEXT_PRIVATE_SMTP_PORT) || 587,
|
port: Number(env('NEXT_PRIVATE_SMTP_PORT')) || 587,
|
||||||
secure: process.env.NEXT_PRIVATE_SMTP_SECURE === 'true',
|
secure: env('NEXT_PRIVATE_SMTP_SECURE') === 'true',
|
||||||
auth: {
|
auth: {
|
||||||
user: process.env.NEXT_PRIVATE_SMTP_APIKEY_USER ?? 'apikey',
|
user: env('NEXT_PRIVATE_SMTP_APIKEY_USER') ?? 'apikey',
|
||||||
pass: process.env.NEXT_PRIVATE_SMTP_APIKEY ?? '',
|
pass: env('NEXT_PRIVATE_SMTP_APIKEY') ?? '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return createTransport({
|
return createTransport({
|
||||||
host: process.env.NEXT_PRIVATE_SMTP_HOST ?? '127.0.0.1:2500',
|
host: env('NEXT_PRIVATE_SMTP_HOST') ?? '127.0.0.1:2500',
|
||||||
port: Number(process.env.NEXT_PRIVATE_SMTP_PORT) || 587,
|
port: Number(env('NEXT_PRIVATE_SMTP_PORT')) || 587,
|
||||||
secure: process.env.NEXT_PRIVATE_SMTP_SECURE === 'true',
|
secure: env('NEXT_PRIVATE_SMTP_SECURE') === 'true',
|
||||||
ignoreTLS: process.env.NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS === 'true',
|
ignoreTLS: env('NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS') === 'true',
|
||||||
auth: process.env.NEXT_PRIVATE_SMTP_USERNAME
|
auth: env('NEXT_PRIVATE_SMTP_USERNAME')
|
||||||
? {
|
? {
|
||||||
user: process.env.NEXT_PRIVATE_SMTP_USERNAME,
|
user: env('NEXT_PRIVATE_SMTP_USERNAME'),
|
||||||
pass: process.env.NEXT_PRIVATE_SMTP_PASSWORD ?? '',
|
pass: env('NEXT_PRIVATE_SMTP_PASSWORD') ?? '',
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
...(process.env.NEXT_PRIVATE_SMTP_SERVICE
|
...(env('NEXT_PRIVATE_SMTP_SERVICE') ? { service: env('NEXT_PRIVATE_SMTP_SERVICE') } : {}),
|
||||||
? { service: process.env.NEXT_PRIVATE_SMTP_SERVICE }
|
|
||||||
: {}),
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/macro';
|
||||||
import { env } from 'next-runtime-env';
|
|
||||||
|
import { env } from '@documenso/lib/utils/env';
|
||||||
|
|
||||||
import { Button, Column, Img, Link, Section, Text } from '../components';
|
import { Button, Column, Img, Link, Section, Text } from '../components';
|
||||||
import { TemplateDocumentImage } from './template-document-image';
|
import { TemplateDocumentImage } from './template-document-image';
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/macro';
|
||||||
import { env } from 'next-runtime-env';
|
|
||||||
|
import { env } from '@documenso/lib/utils/env';
|
||||||
|
|
||||||
import { Button, Section, Text } from '../components';
|
import { Button, Section, Text } from '../components';
|
||||||
import { TemplateDocumentImage } from './template-document-image';
|
import { TemplateDocumentImage } from './template-document-image';
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { SentMessageInfo, Transport } from 'nodemailer';
|
import type { SentMessageInfo, Transport } from 'nodemailer';
|
||||||
import type { Address } from 'nodemailer/lib/mailer';
|
import type { Address } from 'nodemailer/lib/mailer';
|
||||||
import type MailMessage from 'nodemailer/lib/mailer/mail-message';
|
import type MailMessage from 'nodemailer/lib/mailer/mail-message';
|
||||||
|
|
||||||
|
import { env } from '@documenso/lib/utils/env';
|
||||||
|
|
||||||
const VERSION = '1.0.0';
|
const VERSION = '1.0.0';
|
||||||
|
|
||||||
type NodeMailerAddress = string | Address | Array<string | Address> | undefined;
|
type NodeMailerAddress = string | Address | Array<string | Address> | undefined;
|
||||||
@ -79,9 +81,9 @@ export class MailChannelsTransport implements Transport<SentMessageInfo> {
|
|||||||
to: mailTo,
|
to: mailTo,
|
||||||
cc: mailCc.length > 0 ? mailCc : undefined,
|
cc: mailCc.length > 0 ? mailCc : undefined,
|
||||||
bcc: mailBcc.length > 0 ? mailBcc : undefined,
|
bcc: mailBcc.length > 0 ? mailBcc : undefined,
|
||||||
dkim_domain: process.env.NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN || undefined,
|
dkim_domain: env('NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN') || undefined,
|
||||||
dkim_selector: process.env.NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR || undefined,
|
dkim_selector: env('NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR') || undefined,
|
||||||
dkim_private_key: process.env.NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY || undefined,
|
dkim_private_key: env('NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY') || undefined,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
content: [
|
content: [
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
SUPPORTED_LANGUAGE_CODES,
|
SUPPORTED_LANGUAGE_CODES,
|
||||||
isValidLanguageCode,
|
isValidLanguageCode,
|
||||||
} from '../../constants/i18n';
|
} from '../../constants/i18n';
|
||||||
|
import { env } from '../../utils/env';
|
||||||
import { remember } from '../../utils/remember';
|
import { remember } from '../../utils/remember';
|
||||||
|
|
||||||
type SupportedLanguages = (typeof SUPPORTED_LANGUAGE_CODES)[number];
|
type SupportedLanguages = (typeof SUPPORTED_LANGUAGE_CODES)[number];
|
||||||
@ -13,7 +14,7 @@ type SupportedLanguages = (typeof SUPPORTED_LANGUAGE_CODES)[number];
|
|||||||
export async function loadCatalog(lang: SupportedLanguages): Promise<{
|
export async function loadCatalog(lang: SupportedLanguages): Promise<{
|
||||||
[k: string]: Messages;
|
[k: string]: Messages;
|
||||||
}> {
|
}> {
|
||||||
const extension = process.env.NODE_ENV === 'development' ? 'po' : 'js';
|
const extension = env('NODE_ENV') === 'development' ? 'po' : 'js';
|
||||||
|
|
||||||
// const { messages } = await import(`../../translations/${lang}/web.${extension}`);
|
// const { messages } = await import(`../../translations/${lang}/web.${extension}`);
|
||||||
const messages = {};
|
const messages = {};
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
import { env } from 'next-runtime-env';
|
import { env } from '@documenso/lib/utils/env';
|
||||||
|
|
||||||
export const APP_DOCUMENT_UPLOAD_SIZE_LIMIT =
|
export const APP_DOCUMENT_UPLOAD_SIZE_LIMIT =
|
||||||
Number(process.env.NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT) || 50;
|
Number(env('NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT')) || 50;
|
||||||
|
|
||||||
// Todo: env('NEXT_PUBLIC_WEBAPP_URL')
|
// Todo: env('NEXT_PUBLIC_WEBAPP_URL')
|
||||||
export const NEXT_PUBLIC_WEBAPP_URL = () => 'http://localhost:3000';
|
export const NEXT_PUBLIC_WEBAPP_URL = () => 'http://localhost:3000';
|
||||||
export const NEXT_PUBLIC_MARKETING_URL = () => env('NEXT_PUBLIC_MARKETING_URL');
|
export const NEXT_PUBLIC_MARKETING_URL = () => env('NEXT_PUBLIC_MARKETING_URL');
|
||||||
export const NEXT_PRIVATE_INTERNAL_WEBAPP_URL =
|
export const NEXT_PRIVATE_INTERNAL_WEBAPP_URL =
|
||||||
process.env.NEXT_PRIVATE_INTERNAL_WEBAPP_URL ?? NEXT_PUBLIC_WEBAPP_URL();
|
env('NEXT_PRIVATE_INTERNAL_WEBAPP_URL') ?? NEXT_PUBLIC_WEBAPP_URL();
|
||||||
|
|
||||||
export const IS_APP_MARKETING = process.env.NEXT_PUBLIC_PROJECT === 'marketing';
|
export const IS_APP_MARKETING = env('NEXT_PUBLIC_PROJECT') === 'marketing';
|
||||||
export const IS_APP_WEB = process.env.NEXT_PUBLIC_PROJECT === 'web';
|
export const IS_APP_WEB = env('NEXT_PUBLIC_PROJECT') === 'web';
|
||||||
export const IS_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED') === 'true';
|
export const IS_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED') === 'true';
|
||||||
export const IS_APP_WEB_I18N_ENABLED = true;
|
export const IS_APP_WEB_I18N_ENABLED = true;
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Todo: Reimport
|
import { env } from '../utils/env';
|
||||||
|
|
||||||
export const SALT_ROUNDS = 12;
|
export const SALT_ROUNDS = 12;
|
||||||
|
|
||||||
@ -9,17 +9,16 @@ export const IDENTITY_PROVIDER_NAME: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const IS_GOOGLE_SSO_ENABLED = Boolean(
|
export const IS_GOOGLE_SSO_ENABLED = Boolean(
|
||||||
import.meta.env.NEXT_PRIVATE_GOOGLE_CLIENT_ID &&
|
env('NEXT_PRIVATE_GOOGLE_CLIENT_ID') && env('NEXT_PRIVATE_GOOGLE_CLIENT_SECRET'),
|
||||||
import.meta.env.NEXT_PRIVATE_GOOGLE_CLIENT_SECRET,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const IS_OIDC_SSO_ENABLED = Boolean(
|
export const IS_OIDC_SSO_ENABLED = Boolean(
|
||||||
process.env.NEXT_PRIVATE_OIDC_WELL_KNOWN &&
|
env('NEXT_PRIVATE_OIDC_WELL_KNOWN') &&
|
||||||
process.env.NEXT_PRIVATE_OIDC_CLIENT_ID &&
|
env('NEXT_PRIVATE_OIDC_CLIENT_ID') &&
|
||||||
process.env.NEXT_PRIVATE_OIDC_CLIENT_SECRET,
|
env('NEXT_PRIVATE_OIDC_CLIENT_SECRET'),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const OIDC_PROVIDER_LABEL = process.env.NEXT_PRIVATE_OIDC_PROVIDER_LABEL;
|
export const OIDC_PROVIDER_LABEL = env('NEXT_PRIVATE_OIDC_PROVIDER_LABEL');
|
||||||
|
|
||||||
export const USER_SECURITY_AUDIT_LOG_MAP: Record<string, string> = {
|
export const USER_SECURITY_AUDIT_LOG_MAP: Record<string, string> = {
|
||||||
ACCOUNT_SSO_LINK: 'Linked account to SSO',
|
ACCOUNT_SSO_LINK: 'Linked account to SSO',
|
||||||
@ -49,7 +48,7 @@ export const PASSKEY_TIMEOUT = 60000;
|
|||||||
export const MAXIMUM_PASSKEYS = 50;
|
export const MAXIMUM_PASSKEYS = 50;
|
||||||
|
|
||||||
export const useSecureCookies =
|
export const useSecureCookies =
|
||||||
process.env.NODE_ENV === 'production' && String(process.env.NEXTAUTH_URL).startsWith('https://');
|
env('NODE_ENV') === 'production' && String(env('NEXTAUTH_URL')).startsWith('https://');
|
||||||
|
|
||||||
const secureCookiePrefix = useSecureCookies ? '__Secure-' : '';
|
const secureCookiePrefix = useSecureCookies ? '__Secure-' : '';
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
export const FROM_ADDRESS = process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com';
|
import { env } from '../utils/env';
|
||||||
export const FROM_NAME = process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso';
|
|
||||||
|
export const FROM_ADDRESS = env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com';
|
||||||
|
export const FROM_NAME = env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso';
|
||||||
|
|
||||||
export const SERVICE_USER_EMAIL = 'serviceaccount@documenso.com';
|
export const SERVICE_USER_EMAIL = 'serviceaccount@documenso.com';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { env } from 'next-runtime-env';
|
import { env } from '@documenso/lib/utils/env';
|
||||||
|
|
||||||
import { APP_BASE_URL, WEBAPP_BASE_URL } from './app';
|
import { APP_BASE_URL, WEBAPP_BASE_URL } from './app';
|
||||||
|
|
||||||
|
|||||||
@ -17,8 +17,6 @@ export enum AppErrorCode {
|
|||||||
'RETRY_EXCEPTION' = 'RETRY_EXCEPTION',
|
'RETRY_EXCEPTION' = 'RETRY_EXCEPTION',
|
||||||
'SCHEMA_FAILED' = 'SCHEMA_FAILED',
|
'SCHEMA_FAILED' = 'SCHEMA_FAILED',
|
||||||
'TOO_MANY_REQUESTS' = 'TOO_MANY_REQUESTS',
|
'TOO_MANY_REQUESTS' = 'TOO_MANY_REQUESTS',
|
||||||
'PROFILE_URL_TAKEN' = 'PROFILE_URL_TAKEN',
|
|
||||||
'PREMIUM_PROFILE_URL' = 'PREMIUM_PROFILE_URL',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const genericErrorCodeToTrpcErrorCodeMap: Record<string, { code: string; status: number }> =
|
export const genericErrorCodeToTrpcErrorCodeMap: Record<string, { code: string; status: number }> =
|
||||||
@ -34,8 +32,6 @@ export const genericErrorCodeToTrpcErrorCodeMap: Record<string, { code: string;
|
|||||||
[AppErrorCode.RETRY_EXCEPTION]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
|
[AppErrorCode.RETRY_EXCEPTION]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
|
||||||
[AppErrorCode.SCHEMA_FAILED]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
|
[AppErrorCode.SCHEMA_FAILED]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
|
||||||
[AppErrorCode.TOO_MANY_REQUESTS]: { code: 'TOO_MANY_REQUESTS', status: 429 },
|
[AppErrorCode.TOO_MANY_REQUESTS]: { code: 'TOO_MANY_REQUESTS', status: 429 },
|
||||||
[AppErrorCode.PROFILE_URL_TAKEN]: { code: 'BAD_REQUEST', status: 400 },
|
|
||||||
[AppErrorCode.PREMIUM_PROFILE_URL]: { code: 'BAD_REQUEST', status: 400 },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZAppErrorJsonSchema = z.object({
|
export const ZAppErrorJsonSchema = z.object({
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
|
import { env } from '../../utils/env';
|
||||||
import type { JobDefinition, TriggerJobOptions } from './_internal/job';
|
import type { JobDefinition, TriggerJobOptions } from './_internal/job';
|
||||||
import type { BaseJobProvider as JobClientProvider } from './base';
|
import type { BaseJobProvider as JobClientProvider } from './base';
|
||||||
import { InngestJobProvider } from './inngest';
|
import { InngestJobProvider } from './inngest';
|
||||||
@ -10,7 +11,7 @@ export class JobClient<T extends ReadonlyArray<JobDefinition> = []> {
|
|||||||
private _provider: JobClientProvider;
|
private _provider: JobClientProvider;
|
||||||
|
|
||||||
public constructor(definitions: T) {
|
public constructor(definitions: T) {
|
||||||
this._provider = match(process.env.NEXT_PRIVATE_JOBS_PROVIDER)
|
this._provider = match(env('NEXT_PRIVATE_JOBS_PROVIDER'))
|
||||||
.with('inngest', () => InngestJobProvider.getInstance())
|
.with('inngest', () => InngestJobProvider.getInstance())
|
||||||
.with('trigger', () => TriggerJobProvider.getInstance())
|
.with('trigger', () => TriggerJobProvider.getInstance())
|
||||||
.otherwise(() => LocalJobProvider.getInstance());
|
.otherwise(() => LocalJobProvider.getInstance());
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import type { Logger } from 'inngest/middleware/logger';
|
|||||||
import { serve as createPagesRoute } from 'inngest/next';
|
import { serve as createPagesRoute } from 'inngest/next';
|
||||||
import { json } from 'micro';
|
import { json } from 'micro';
|
||||||
|
|
||||||
|
import { env } from '../../utils/env';
|
||||||
import type { JobDefinition, JobRunIO, SimpleTriggerJobOptions } from './_internal/job';
|
import type { JobDefinition, JobRunIO, SimpleTriggerJobOptions } from './_internal/job';
|
||||||
import { BaseJobProvider } from './base';
|
import { BaseJobProvider } from './base';
|
||||||
|
|
||||||
@ -28,8 +29,8 @@ export class InngestJobProvider extends BaseJobProvider {
|
|||||||
static getInstance() {
|
static getInstance() {
|
||||||
if (!this._instance) {
|
if (!this._instance) {
|
||||||
const client = new InngestClient({
|
const client = new InngestClient({
|
||||||
id: process.env.NEXT_PRIVATE_INNGEST_APP_ID || 'documenso-app',
|
id: env('NEXT_PRIVATE_INNGEST_APP_ID') || 'documenso-app',
|
||||||
eventKey: process.env.INNGEST_EVENT_KEY || process.env.NEXT_PRIVATE_INNGEST_EVENT_KEY,
|
eventKey: env('INNGEST_EVENT_KEY') || env('NEXT_PRIVATE_INNGEST_EVENT_KEY'),
|
||||||
});
|
});
|
||||||
|
|
||||||
this._instance = new InngestJobProvider({ client });
|
this._instance = new InngestJobProvider({ client });
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { createPagesRoute } from '@trigger.dev/nextjs';
|
|||||||
import type { IO } from '@trigger.dev/sdk';
|
import type { IO } from '@trigger.dev/sdk';
|
||||||
import { TriggerClient, eventTrigger } from '@trigger.dev/sdk';
|
import { TriggerClient, eventTrigger } from '@trigger.dev/sdk';
|
||||||
|
|
||||||
|
import { env } from '../../utils/env';
|
||||||
import type { JobDefinition, JobRunIO, SimpleTriggerJobOptions } from './_internal/job';
|
import type { JobDefinition, JobRunIO, SimpleTriggerJobOptions } from './_internal/job';
|
||||||
import { BaseJobProvider } from './base';
|
import { BaseJobProvider } from './base';
|
||||||
|
|
||||||
@ -20,8 +21,8 @@ export class TriggerJobProvider extends BaseJobProvider {
|
|||||||
if (!this._instance) {
|
if (!this._instance) {
|
||||||
const client = new TriggerClient({
|
const client = new TriggerClient({
|
||||||
id: 'documenso-app',
|
id: 'documenso-app',
|
||||||
apiKey: process.env.NEXT_PRIVATE_TRIGGER_API_KEY,
|
apiKey: env('NEXT_PRIVATE_TRIGGER_API_KEY'),
|
||||||
apiUrl: process.env.NEXT_PRIVATE_TRIGGER_API_URL,
|
apiUrl: env('NEXT_PRIVATE_TRIGGER_API_URL'),
|
||||||
});
|
});
|
||||||
|
|
||||||
this._instance = new TriggerJobProvider({ client });
|
this._instance = new TriggerJobProvider({ client });
|
||||||
|
|||||||
@ -10,8 +10,8 @@ import type { JWT } from 'next-auth/jwt';
|
|||||||
import CredentialsProvider from 'next-auth/providers/credentials';
|
import CredentialsProvider from 'next-auth/providers/credentials';
|
||||||
import type { GoogleProfile } from 'next-auth/providers/google';
|
import type { GoogleProfile } from 'next-auth/providers/google';
|
||||||
import GoogleProvider from 'next-auth/providers/google';
|
import GoogleProvider from 'next-auth/providers/google';
|
||||||
import { env } from 'next-runtime-env';
|
|
||||||
|
|
||||||
|
import { env } from '@documenso/lib/utils/env';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { formatSecureCookieName, useSecureCookies } from '../constants/auth';
|
import { formatSecureCookieName, useSecureCookies } from '../constants/auth';
|
||||||
|
|||||||
@ -1,24 +1,28 @@
|
|||||||
export const isErrorCode = (code: unknown): code is ErrorCode => {
|
// export const isErrorCode = (code: unknown): code is ErrorCode => {
|
||||||
return typeof code === 'string' && code in ErrorCode;
|
// return typeof code === 'string' && code in ErrorCode;
|
||||||
};
|
// };
|
||||||
|
|
||||||
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
// export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
||||||
|
|
||||||
export const ErrorCode = {
|
// Todo: Delete file
|
||||||
INCORRECT_EMAIL_PASSWORD: 'INCORRECT_EMAIL_PASSWORD',
|
// Todo: Delete file
|
||||||
USER_MISSING_PASSWORD: 'USER_MISSING_PASSWORD',
|
// Todo: Delete file
|
||||||
CREDENTIALS_NOT_FOUND: 'CREDENTIALS_NOT_FOUND',
|
// Todo: Delete file
|
||||||
INTERNAL_SEVER_ERROR: 'INTERNAL_SEVER_ERROR',
|
// export const ErrorCode = {
|
||||||
TWO_FACTOR_ALREADY_ENABLED: 'TWO_FACTOR_ALREADY_ENABLED',
|
// INCORRECT_EMAIL_PASSWORD: 'INCORRECT_EMAIL_PASSWORD',
|
||||||
TWO_FACTOR_SETUP_REQUIRED: 'TWO_FACTOR_SETUP_REQUIRED',
|
// USER_MISSING_PASSWORD: 'USER_MISSING_PASSWORD',
|
||||||
TWO_FACTOR_MISSING_SECRET: 'TWO_FACTOR_MISSING_SECRET',
|
// CREDENTIALS_NOT_FOUND: 'CREDENTIALS_NOT_FOUND',
|
||||||
TWO_FACTOR_MISSING_CREDENTIALS: 'TWO_FACTOR_MISSING_CREDENTIALS',
|
// INTERNAL_SEVER_ERROR: 'INTERNAL_SEVER_ERROR',
|
||||||
INCORRECT_TWO_FACTOR_CODE: 'INCORRECT_TWO_FACTOR_CODE',
|
// TWO_FACTOR_ALREADY_ENABLED: 'TWO_FACTOR_ALREADY_ENABLED',
|
||||||
INCORRECT_TWO_FACTOR_BACKUP_CODE: 'INCORRECT_TWO_FACTOR_BACKUP_CODE',
|
// TWO_FACTOR_SETUP_REQUIRED: 'TWO_FACTOR_SETUP_REQUIRED',
|
||||||
INCORRECT_IDENTITY_PROVIDER: 'INCORRECT_IDENTITY_PROVIDER',
|
// TWO_FACTOR_MISSING_SECRET: 'TWO_FACTOR_MISSING_SECRET',
|
||||||
INCORRECT_PASSWORD: 'INCORRECT_PASSWORD',
|
// TWO_FACTOR_MISSING_CREDENTIALS: 'TWO_FACTOR_MISSING_CREDENTIALS',
|
||||||
MISSING_ENCRYPTION_KEY: 'MISSING_ENCRYPTION_KEY',
|
// INCORRECT_TWO_FACTOR_CODE: 'INCORRECT_TWO_FACTOR_CODE',
|
||||||
MISSING_BACKUP_CODE: 'MISSING_BACKUP_CODE',
|
// INCORRECT_TWO_FACTOR_BACKUP_CODE: 'INCORRECT_TWO_FACTOR_BACKUP_CODE',
|
||||||
UNVERIFIED_EMAIL: 'UNVERIFIED_EMAIL',
|
// INCORRECT_IDENTITY_PROVIDER: 'INCORRECT_IDENTITY_PROVIDER',
|
||||||
ACCOUNT_DISABLED: 'ACCOUNT_DISABLED',
|
// INCORRECT_PASSWORD: 'INCORRECT_PASSWORD',
|
||||||
} as const;
|
// MISSING_ENCRYPTION_KEY: 'MISSING_ENCRYPTION_KEY',
|
||||||
|
// MISSING_BACKUP_CODE: 'MISSING_BACKUP_CODE',
|
||||||
|
// UNVERIFIED_EMAIL: 'UNVERIFIED_EMAIL',
|
||||||
|
// ACCOUNT_DISABLED: 'ACCOUNT_DISABLED',
|
||||||
|
// } as const;
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { base32 } from '@scure/base';
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { createTOTPKeyURI } from 'oslo/otp';
|
import { createTOTPKeyURI } from 'oslo/otp';
|
||||||
|
|
||||||
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
|
import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
|
||||||
@ -21,7 +20,7 @@ export const setupTwoFactorAuthentication = async ({
|
|||||||
const key = DOCUMENSO_ENCRYPTION_KEY;
|
const key = DOCUMENSO_ENCRYPTION_KEY;
|
||||||
|
|
||||||
if (!key) {
|
if (!key) {
|
||||||
throw new Error(ErrorCode.MISSING_ENCRYPTION_KEY);
|
throw new Error('MISSING_ENCRYPTION_KEY');
|
||||||
}
|
}
|
||||||
|
|
||||||
const secret = crypto.randomBytes(10);
|
const secret = crypto.randomBytes(10);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { User } from '@prisma/client';
|
import type { User } from '@prisma/client';
|
||||||
|
|
||||||
import { ErrorCode } from '../../next-auth/error-codes';
|
import { AppError } from '../../errors/app-error';
|
||||||
import { verifyTwoFactorAuthenticationToken } from './verify-2fa-token';
|
import { verifyTwoFactorAuthenticationToken } from './verify-2fa-token';
|
||||||
import { verifyBackupCode } from './verify-backup-code';
|
import { verifyBackupCode } from './verify-backup-code';
|
||||||
|
|
||||||
@ -16,11 +16,11 @@ export const validateTwoFactorAuthentication = async ({
|
|||||||
user,
|
user,
|
||||||
}: ValidateTwoFactorAuthenticationOptions) => {
|
}: ValidateTwoFactorAuthenticationOptions) => {
|
||||||
if (!user.twoFactorEnabled) {
|
if (!user.twoFactorEnabled) {
|
||||||
throw new Error(ErrorCode.TWO_FACTOR_SETUP_REQUIRED);
|
throw new AppError('TWO_FACTOR_SETUP_REQUIRED');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.twoFactorSecret) {
|
if (!user.twoFactorSecret) {
|
||||||
throw new Error(ErrorCode.TWO_FACTOR_MISSING_SECRET);
|
throw new AppError('TWO_FACTOR_MISSING_SECRET');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totpCode) {
|
if (totpCode) {
|
||||||
@ -31,5 +31,5 @@ export const validateTwoFactorAuthentication = async ({
|
|||||||
return await verifyBackupCode({ user, backupCode });
|
return await verifyBackupCode({ user, backupCode });
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(ErrorCode.TWO_FACTOR_MISSING_CREDENTIALS);
|
throw new AppError('TWO_FACTOR_MISSING_CREDENTIALS');
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { prisma } from '@documenso/prisma';
|
|||||||
|
|
||||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||||
|
import { env } from '../../utils/env';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
|
|
||||||
export interface SendConfirmationEmailProps {
|
export interface SendConfirmationEmailProps {
|
||||||
@ -15,8 +16,8 @@ export interface SendConfirmationEmailProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const sendConfirmationEmail = async ({ userId }: SendConfirmationEmailProps) => {
|
export const sendConfirmationEmail = async ({ userId }: SendConfirmationEmailProps) => {
|
||||||
const NEXT_PRIVATE_SMTP_FROM_NAME = process.env.NEXT_PRIVATE_SMTP_FROM_NAME;
|
const NEXT_PRIVATE_SMTP_FROM_NAME = env('NEXT_PRIVATE_SMTP_FROM_NAME');
|
||||||
const NEXT_PRIVATE_SMTP_FROM_ADDRESS = process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS;
|
const NEXT_PRIVATE_SMTP_FROM_ADDRESS = env('NEXT_PRIVATE_SMTP_FROM_ADDRESS');
|
||||||
|
|
||||||
const user = await prisma.user.findFirstOrThrow({
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { prisma } from '@documenso/prisma';
|
|||||||
|
|
||||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||||
|
import { env } from '../../utils/env';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
|
|
||||||
export interface SendForgotPasswordOptions {
|
export interface SendForgotPasswordOptions {
|
||||||
@ -55,8 +56,8 @@ export const sendForgotPassword = async ({ userId }: SendForgotPasswordOptions)
|
|||||||
name: user.name || '',
|
name: user.name || '',
|
||||||
},
|
},
|
||||||
from: {
|
from: {
|
||||||
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
|
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
|
||||||
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
|
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
|
||||||
},
|
},
|
||||||
subject: i18n._(msg`Forgot Password?`),
|
subject: i18n._(msg`Forgot Password?`),
|
||||||
html,
|
html,
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { ResetPasswordTemplate } from '@documenso/email/templates/reset-password
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||||
|
import { env } from '../../utils/env';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
|
|
||||||
export interface SendResetPasswordOptions {
|
export interface SendResetPasswordOptions {
|
||||||
@ -37,8 +38,8 @@ export const sendResetPassword = async ({ userId }: SendResetPasswordOptions) =>
|
|||||||
name: user.name || '',
|
name: user.name || '',
|
||||||
},
|
},
|
||||||
from: {
|
from: {
|
||||||
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
|
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
|
||||||
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
|
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
|
||||||
},
|
},
|
||||||
subject: 'Password Reset Success!',
|
subject: 'Password Reset Success!',
|
||||||
html,
|
html,
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { extractDerivedDocumentEmailSettings } from '../../types/document-email'
|
|||||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||||
import { getFile } from '../../universal/upload/get-file';
|
import { getFile } from '../../universal/upload/get-file';
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
|
import { env } from '../../utils/env';
|
||||||
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
|
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||||
@ -113,8 +114,8 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
from: {
|
from: {
|
||||||
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
|
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
|
||||||
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
|
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
|
||||||
},
|
},
|
||||||
subject: i18n._(msg`Signing Complete!`),
|
subject: i18n._(msg`Signing Complete!`),
|
||||||
html,
|
html,
|
||||||
@ -190,8 +191,8 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
from: {
|
from: {
|
||||||
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
|
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
|
||||||
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
|
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
|
||||||
},
|
},
|
||||||
subject:
|
subject:
|
||||||
isDirectTemplate && document.documentMeta?.subject
|
isDirectTemplate && document.documentMeta?.subject
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
|||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
|
import { env } from '../../utils/env';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||||
|
|
||||||
@ -79,8 +80,8 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
|
|||||||
name: name || '',
|
name: name || '',
|
||||||
},
|
},
|
||||||
from: {
|
from: {
|
||||||
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
|
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
|
||||||
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
|
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
|
||||||
},
|
},
|
||||||
subject: i18n._(msg`Document Deleted!`),
|
subject: i18n._(msg`Document Deleted!`),
|
||||||
html,
|
html,
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { prisma } from '@documenso/prisma';
|
|||||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
|
import { env } from '../../utils/env';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||||
|
|
||||||
@ -90,8 +91,8 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
|
|||||||
name,
|
name,
|
||||||
},
|
},
|
||||||
from: {
|
from: {
|
||||||
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
|
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
|
||||||
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
|
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
|
||||||
},
|
},
|
||||||
subject: i18n._(msg`Waiting for others to complete signing.`),
|
subject: i18n._(msg`Waiting for others to complete signing.`),
|
||||||
html,
|
html,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import type { Browser } from 'playwright';
|
|||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||||
import { type SupportedLanguageCodes, isValidLanguageCode } from '../../constants/i18n';
|
import { type SupportedLanguageCodes, isValidLanguageCode } from '../../constants/i18n';
|
||||||
|
import { env } from '../../utils/env';
|
||||||
import { encryptSecondaryData } from '../crypto/encrypt';
|
import { encryptSecondaryData } from '../crypto/encrypt';
|
||||||
|
|
||||||
export type GetCertificatePdfOptions = {
|
export type GetCertificatePdfOptions = {
|
||||||
@ -21,10 +22,10 @@ export const getCertificatePdf = async ({ documentId, language }: GetCertificate
|
|||||||
|
|
||||||
let browser: Browser;
|
let browser: Browser;
|
||||||
|
|
||||||
if (process.env.NEXT_PRIVATE_BROWSERLESS_URL) {
|
if (env('NEXT_PRIVATE_BROWSERLESS_URL')) {
|
||||||
// !: Use CDP rather than the default `connect` method to avoid coupling to the playwright version.
|
// !: Use CDP rather than the default `connect` method to avoid coupling to the playwright version.
|
||||||
// !: Previously we would have to keep the playwright version in sync with the browserless version to avoid errors.
|
// !: Previously we would have to keep the playwright version in sync with the browserless version to avoid errors.
|
||||||
browser = await chromium.connectOverCDP(process.env.NEXT_PRIVATE_BROWSERLESS_URL);
|
browser = await chromium.connectOverCDP(env('NEXT_PRIVATE_BROWSERLESS_URL'));
|
||||||
} else {
|
} else {
|
||||||
browser = await chromium.launch();
|
browser = await chromium.launch();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,15 +25,12 @@ import {
|
|||||||
ZRadioFieldMeta,
|
ZRadioFieldMeta,
|
||||||
ZTextFieldMeta,
|
ZTextFieldMeta,
|
||||||
} from '../../types/field-meta';
|
} from '../../types/field-meta';
|
||||||
|
import { env } from '../../utils/env';
|
||||||
|
|
||||||
export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignature) => {
|
export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignature) => {
|
||||||
const fontCaveat = await fetch(process.env.FONT_CAVEAT_URI).then(async (res) =>
|
const fontCaveat = await fetch(env('FONT_CAVEAT_URI')).then(async (res) => res.arrayBuffer());
|
||||||
res.arrayBuffer(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const fontNoto = await fetch(process.env.FONT_NOTO_SANS_URI).then(async (res) =>
|
const fontNoto = await fetch(env('FONT_NOTO_SANS_URI')).then(async (res) => res.arrayBuffer());
|
||||||
res.arrayBuffer(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const isSignatureField = isSignatureFieldType(field.type);
|
const isSignatureField = isSignatureFieldType(field.type);
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||||
import { Redis } from '@upstash/redis';
|
import { Redis } from '@upstash/redis';
|
||||||
|
|
||||||
|
import { env } from '../../utils/env';
|
||||||
|
|
||||||
// !: We're null coalescing here because we don't want local builds to fail.
|
// !: We're null coalescing here because we don't want local builds to fail.
|
||||||
export const redis = new Redis({
|
export const redis = new Redis({
|
||||||
url: process.env.NEXT_PRIVATE_REDIS_URL ?? '',
|
url: env('NEXT_PRIVATE_REDIS_URL') ?? '',
|
||||||
token: process.env.NEXT_PRIVATE_REDIS_TOKEN ?? '',
|
token: env('NEXT_PRIVATE_REDIS_TOKEN') ?? '',
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
/// <reference types="./stripe.d.ts" />
|
/// <reference types="./stripe.d.ts" />
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
export const stripe = new Stripe(process.env.NEXT_PRIVATE_STRIPE_API_KEY ?? '', {
|
import { env } from '../../utils/env';
|
||||||
|
|
||||||
|
export const stripe = new Stripe(env('NEXT_PRIVATE_STRIPE_API_KEY') ?? '', {
|
||||||
apiVersion: '2022-11-15',
|
apiVersion: '2022-11-15',
|
||||||
typescript: true,
|
typescript: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { createTokenVerification } from '@documenso/lib/utils/token-verification
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||||
|
import { env } from '../../utils/env';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||||
|
|
||||||
@ -122,7 +123,7 @@ export const sendTeamEmailVerificationEmail = async (
|
|||||||
teamGlobalSettings?: TeamGlobalSettings | null;
|
teamGlobalSettings?: TeamGlobalSettings | null;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
|
const assetBaseUrl = env('NEXT_PUBLIC_WEBAPP_URL') || 'http://localhost:3000';
|
||||||
|
|
||||||
const template = createElement(ConfirmTeamEmailTemplate, {
|
const template = createElement(ConfirmTeamEmailTemplate, {
|
||||||
assetBaseUrl,
|
assetBaseUrl,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user