mirror of
https://github.com/documenso/documenso.git
synced 2025-11-21 04:01:45 +10:00
Merge branch 'main' into feat/org-insights
This commit is contained in:
@ -136,3 +136,5 @@ E2E_TEST_AUTHENTICATE_USER_PASSWORD="test_Password123"
|
|||||||
# OPTIONAL: The file to save the logger output to. Will disable stdout if provided.
|
# OPTIONAL: The file to save the logger output to. Will disable stdout if provided.
|
||||||
NEXT_PRIVATE_LOGGER_FILE_PATH=
|
NEXT_PRIVATE_LOGGER_FILE_PATH=
|
||||||
|
|
||||||
|
# [[PLAIN SUPPORT]]
|
||||||
|
NEXT_PRIVATE_PLAIN_API_KEY=
|
||||||
|
|||||||
@ -0,0 +1,159 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type { User } from '@prisma/client';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
|
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 { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
export type AdminUserResetTwoFactorDialogProps = {
|
||||||
|
className?: string;
|
||||||
|
user: User;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AdminUserResetTwoFactorDialog = ({
|
||||||
|
className,
|
||||||
|
user,
|
||||||
|
}: AdminUserResetTwoFactorDialogProps) => {
|
||||||
|
const { _ } = useLingui();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const { mutateAsync: resetTwoFactor, isPending: isResettingTwoFactor } =
|
||||||
|
trpc.admin.user.resetTwoFactor.useMutation();
|
||||||
|
|
||||||
|
const onResetTwoFactor = async () => {
|
||||||
|
try {
|
||||||
|
await resetTwoFactor({
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: _(msg`2FA Reset`),
|
||||||
|
description: _(msg`The user's two factor authentication has been reset successfully.`),
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await revalidate();
|
||||||
|
setOpen(false);
|
||||||
|
} catch (err) {
|
||||||
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
|
const errorMessage = match(error.code)
|
||||||
|
.with(AppErrorCode.NOT_FOUND, () => msg`User not found.`)
|
||||||
|
.with(
|
||||||
|
AppErrorCode.UNAUTHORIZED,
|
||||||
|
() => msg`You are not authorized to reset two factor authentcation for this user.`,
|
||||||
|
)
|
||||||
|
.otherwise(
|
||||||
|
() => msg`An error occurred while resetting two factor authentication for the user.`,
|
||||||
|
);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: _(msg`Error`),
|
||||||
|
description: _(errorMessage),
|
||||||
|
variant: 'destructive',
|
||||||
|
duration: 7500,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenChange = (newOpen: boolean) => {
|
||||||
|
setOpen(newOpen);
|
||||||
|
|
||||||
|
if (!newOpen) {
|
||||||
|
setEmail('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<Alert
|
||||||
|
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
|
||||||
|
variant="neutral"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<AlertTitle>Reset Two Factor Authentication</AlertTitle>
|
||||||
|
<AlertDescription className="mr-2">
|
||||||
|
<Trans>
|
||||||
|
Reset the users two factor authentication. This action is irreversible and will
|
||||||
|
disable two factor authentication for the user.
|
||||||
|
</Trans>
|
||||||
|
</AlertDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="destructive">
|
||||||
|
<Trans>Reset 2FA</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader className="space-y-4">
|
||||||
|
<DialogTitle>
|
||||||
|
<Trans>Reset Two Factor Authentication</Trans>
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertDescription className="selection:bg-red-100">
|
||||||
|
<Trans>
|
||||||
|
This action is irreversible. Please ensure you have informed the user before
|
||||||
|
proceeding.
|
||||||
|
</Trans>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<DialogDescription>
|
||||||
|
<Trans>
|
||||||
|
To confirm, please enter the accounts email address <br />({user.email}).
|
||||||
|
</Trans>
|
||||||
|
</DialogDescription>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
className="mt-2"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
disabled={email !== user.email}
|
||||||
|
onClick={onResetTwoFactor}
|
||||||
|
loading={isResettingTwoFactor}
|
||||||
|
>
|
||||||
|
<Trans>Reset 2FA</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
138
apps/remix/app/components/forms/support-ticket-form.tsx
Normal file
138
apps/remix/app/components/forms/support-ticket-form.tsx
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@documenso/ui/primitives/form/form';
|
||||||
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
import { Textarea } from '@documenso/ui/primitives/textarea';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
const ZSupportTicketSchema = z.object({
|
||||||
|
subject: z.string().min(3, 'Subject is required'),
|
||||||
|
message: z.string().min(10, 'Message must be at least 10 characters'),
|
||||||
|
});
|
||||||
|
|
||||||
|
type TSupportTicket = z.infer<typeof ZSupportTicketSchema>;
|
||||||
|
|
||||||
|
export type SupportTicketFormProps = {
|
||||||
|
organisationId: string;
|
||||||
|
teamId?: string | null;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
onClose?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SupportTicketForm = ({
|
||||||
|
organisationId,
|
||||||
|
teamId,
|
||||||
|
onSuccess,
|
||||||
|
onClose,
|
||||||
|
}: SupportTicketFormProps) => {
|
||||||
|
const { t } = useLingui();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const { mutateAsync: submitSupportTicket, isPending } =
|
||||||
|
trpc.profile.submitSupportTicket.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<TSupportTicket>({
|
||||||
|
resolver: zodResolver(ZSupportTicketSchema),
|
||||||
|
defaultValues: {
|
||||||
|
subject: '',
|
||||||
|
message: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isLoading = form.formState.isLoading || isPending;
|
||||||
|
|
||||||
|
const onSubmit = async (data: TSupportTicket) => {
|
||||||
|
const { subject, message } = data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await submitSupportTicket({
|
||||||
|
subject,
|
||||||
|
message,
|
||||||
|
organisationId,
|
||||||
|
teamId,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: t`Support ticket created`,
|
||||||
|
description: t`Your support request has been submitted. We'll get back to you soon!`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (onSuccess) {
|
||||||
|
onSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
form.reset();
|
||||||
|
} catch (err) {
|
||||||
|
toast({
|
||||||
|
title: t`Failed to create support ticket`,
|
||||||
|
description: t`An error occurred. Please try again later.`,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
|
<fieldset disabled={isLoading} className="flex flex-col gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="subject"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel required>
|
||||||
|
<Trans>Subject</Trans>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="message"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel required>
|
||||||
|
<Trans>Message</Trans>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea rows={5} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-2 flex flex-row gap-2">
|
||||||
|
<Button type="submit" size="sm" loading={isLoading}>
|
||||||
|
<Trans>Submit</Trans>
|
||||||
|
</Button>
|
||||||
|
{onClose && (
|
||||||
|
<Button variant="outline" size="sm" type="button" onClick={onClose}>
|
||||||
|
<Trans>Close</Trans>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -321,6 +321,19 @@ export const OrgMenuSwitcher = () => {
|
|||||||
<Trans>Language</Trans>
|
<Trans>Language</Trans>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
{currentOrganisation && (
|
||||||
|
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
|
||||||
|
<Link
|
||||||
|
to={{
|
||||||
|
pathname: `/o/${currentOrganisation.url}/support`,
|
||||||
|
search: currentTeam ? `?team=${currentTeam.id}` : '',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trans>Support</Trans>
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="text-muted-foreground hover:!text-muted-foreground px-4 py-2"
|
className="text-muted-foreground hover:!text-muted-foreground px-4 py-2"
|
||||||
onSelect={async () => authClient.signOut()}
|
onSelect={async () => authClient.signOut()}
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import { AdminOrganisationCreateDialog } from '~/components/dialogs/admin-organi
|
|||||||
import { AdminUserDeleteDialog } from '~/components/dialogs/admin-user-delete-dialog';
|
import { AdminUserDeleteDialog } from '~/components/dialogs/admin-user-delete-dialog';
|
||||||
import { AdminUserDisableDialog } from '~/components/dialogs/admin-user-disable-dialog';
|
import { AdminUserDisableDialog } from '~/components/dialogs/admin-user-disable-dialog';
|
||||||
import { AdminUserEnableDialog } from '~/components/dialogs/admin-user-enable-dialog';
|
import { AdminUserEnableDialog } from '~/components/dialogs/admin-user-enable-dialog';
|
||||||
|
import { AdminUserResetTwoFactorDialog } from '~/components/dialogs/admin-user-reset-two-factor-dialog';
|
||||||
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
|
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
|
||||||
import { AdminOrganisationsTable } from '~/components/tables/admin-organisations-table';
|
import { AdminOrganisationsTable } from '~/components/tables/admin-organisations-table';
|
||||||
|
|
||||||
@ -219,10 +220,11 @@ const AdminUserPage = ({ user }: { user: User }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-16 flex flex-col items-center gap-4">
|
<div className="mt-16 flex flex-col gap-4">
|
||||||
{user && <AdminUserDeleteDialog user={user} />}
|
{user && user.twoFactorEnabled && <AdminUserResetTwoFactorDialog user={user} />}
|
||||||
{user && user.disabled && <AdminUserEnableDialog userToEnable={user} />}
|
{user && user.disabled && <AdminUserEnableDialog userToEnable={user} />}
|
||||||
{user && !user.disabled && <AdminUserDisableDialog userToDisable={user} />}
|
{user && !user.disabled && <AdminUserDisableDialog userToDisable={user} />}
|
||||||
|
{user && <AdminUserDeleteDialog user={user} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
125
apps/remix/app/routes/_authenticated+/o.$orgUrl.support.tsx
Normal file
125
apps/remix/app/routes/_authenticated+/o.$orgUrl.support.tsx
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { BookIcon, HelpCircleIcon, Link2Icon } from 'lucide-react';
|
||||||
|
import { Link, useSearchParams } from 'react-router';
|
||||||
|
|
||||||
|
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||||
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
|
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
|
import { SupportTicketForm } from '~/components/forms/support-ticket-form';
|
||||||
|
import { appMetaTags } from '~/utils/meta';
|
||||||
|
|
||||||
|
export function meta() {
|
||||||
|
return appMetaTags('Support');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SupportPage() {
|
||||||
|
const [showForm, setShowForm] = useState(false);
|
||||||
|
const { user } = useSession();
|
||||||
|
const organisation = useCurrentOrganisation();
|
||||||
|
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const teamId = searchParams.get('team');
|
||||||
|
|
||||||
|
const subscriptionStatus = organisation.subscription?.status;
|
||||||
|
|
||||||
|
const handleSuccess = () => {
|
||||||
|
setShowForm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseForm = () => {
|
||||||
|
setShowForm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="flex flex-row items-center gap-2 text-3xl font-bold">
|
||||||
|
<HelpCircleIcon className="text-muted-foreground h-8 w-8" />
|
||||||
|
<Trans>Support</Trans>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="text-muted-foreground mt-2">
|
||||||
|
<Trans>Your current plan includes the following support channels:</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-6 flex flex-col gap-4">
|
||||||
|
<div className="rounded-lg border p-4">
|
||||||
|
<h2 className="flex items-center gap-2 text-lg font-bold">
|
||||||
|
<BookIcon className="text-muted-foreground h-5 w-5" />
|
||||||
|
<Link
|
||||||
|
to="https://docs.documenso.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="hover:underline"
|
||||||
|
>
|
||||||
|
<Trans>Documentation</Trans>
|
||||||
|
</Link>
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground mt-1">
|
||||||
|
<Trans>Read our documentation to get started with Documenso.</Trans>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border p-4">
|
||||||
|
<h2 className="flex items-center gap-2 text-lg font-bold">
|
||||||
|
<Link2Icon className="text-muted-foreground h-5 w-5" />
|
||||||
|
<Link
|
||||||
|
to="https://documen.so/discord"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="hover:underline"
|
||||||
|
>
|
||||||
|
<Trans>Discord</Trans>
|
||||||
|
</Link>
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground mt-1">
|
||||||
|
<Trans>
|
||||||
|
Join our community on{' '}
|
||||||
|
<Link
|
||||||
|
to="https://documen.so/discord"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="hover:underline"
|
||||||
|
>
|
||||||
|
Discord
|
||||||
|
</Link>{' '}
|
||||||
|
for community support and discussion.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{organisation && IS_BILLING_ENABLED() && subscriptionStatus && (
|
||||||
|
<>
|
||||||
|
<div className="rounded-lg border p-4">
|
||||||
|
<h2 className="flex items-center gap-2 text-lg font-bold">
|
||||||
|
<Link2Icon className="text-muted-foreground h-5 w-5" />
|
||||||
|
<Trans>Contact us</Trans>
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground mt-1">
|
||||||
|
<Trans>We'll get back to you as soon as possible via email.</Trans>
|
||||||
|
</p>
|
||||||
|
<div className="mt-4">
|
||||||
|
{!showForm ? (
|
||||||
|
<Button variant="outline" size="sm" onClick={() => setShowForm(true)}>
|
||||||
|
<Trans>Create a support ticket</Trans>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<SupportTicketForm
|
||||||
|
organisationId={organisation.id}
|
||||||
|
teamId={teamId}
|
||||||
|
onSuccess={handleSuccess}
|
||||||
|
onClose={handleCloseForm}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -101,5 +101,5 @@
|
|||||||
"vite-plugin-babel-macros": "^1.0.6",
|
"vite-plugin-babel-macros": "^1.0.6",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
},
|
},
|
||||||
"version": "1.12.2-rc.3"
|
"version": "1.12.2-rc.4"
|
||||||
}
|
}
|
||||||
|
|||||||
60
package-lock.json
generated
60
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "1.12.2-rc.3",
|
"version": "1.12.2-rc.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "1.12.2-rc.3",
|
"version": "1.12.2-rc.4",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
@ -89,7 +89,7 @@
|
|||||||
},
|
},
|
||||||
"apps/remix": {
|
"apps/remix": {
|
||||||
"name": "@documenso/remix",
|
"name": "@documenso/remix",
|
||||||
"version": "1.12.2-rc.3",
|
"version": "1.12.2-rc.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/api": "*",
|
"@documenso/api": "*",
|
||||||
"@documenso/assets": "*",
|
"@documenso/assets": "*",
|
||||||
@ -3522,6 +3522,15 @@
|
|||||||
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@graphql-typed-document-node/core": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@grpc/grpc-js": {
|
"node_modules/@grpc/grpc-js": {
|
||||||
"version": "1.13.3",
|
"version": "1.13.3",
|
||||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.3.tgz",
|
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.3.tgz",
|
||||||
@ -11826,6 +11835,20 @@
|
|||||||
"url": "https://github.com/sponsors/tannerlinsley"
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@team-plain/typescript-sdk": {
|
||||||
|
"version": "5.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@team-plain/typescript-sdk/-/typescript-sdk-5.9.0.tgz",
|
||||||
|
"integrity": "sha512-AHSXyt1kDt74m9YKZBCRCd6cQjB8QjUNr9cehtR2QHzZ/8yXJPzawPJDqOQ3ms5KvwuYrBx2qT3e6C/zrQ5UtA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@graphql-typed-document-node/core": "^3.2.0",
|
||||||
|
"ajv": "^8.12.0",
|
||||||
|
"ajv-formats": "^2.1.1",
|
||||||
|
"graphql": "^16.6.0",
|
||||||
|
"lodash.get": "^4.4.2",
|
||||||
|
"zod": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@theguild/remark-mermaid": {
|
"node_modules/@theguild/remark-mermaid": {
|
||||||
"version": "0.0.5",
|
"version": "0.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@theguild/remark-mermaid/-/remark-mermaid-0.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@theguild/remark-mermaid/-/remark-mermaid-0.0.5.tgz",
|
||||||
@ -13235,7 +13258,6 @@
|
|||||||
"version": "8.17.1",
|
"version": "8.17.1",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
@ -13248,6 +13270,23 @@
|
|||||||
"url": "https://github.com/sponsors/epoberezkin"
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ajv-formats": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ajv": "^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"ajv": "^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"ajv": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansi-escapes": {
|
"node_modules/ansi-escapes": {
|
||||||
"version": "4.3.2",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
||||||
@ -18771,7 +18810,6 @@
|
|||||||
"version": "3.0.6",
|
"version": "3.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
||||||
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
|
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@ -19847,6 +19885,15 @@
|
|||||||
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
|
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/graphql": {
|
||||||
|
"version": "16.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz",
|
||||||
|
"integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gray-matter": {
|
"node_modules/gray-matter": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
|
||||||
@ -22329,7 +22376,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/json-stable-stringify-without-jsonify": {
|
"node_modules/json-stable-stringify-without-jsonify": {
|
||||||
@ -30570,7 +30616,6 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@ -36583,6 +36628,7 @@
|
|||||||
"@pdf-lib/fontkit": "^1.1.1",
|
"@pdf-lib/fontkit": "^1.1.1",
|
||||||
"@scure/base": "^1.1.3",
|
"@scure/base": "^1.1.3",
|
||||||
"@sindresorhus/slugify": "^2.2.1",
|
"@sindresorhus/slugify": "^2.2.1",
|
||||||
|
"@team-plain/typescript-sdk": "^5.9.0",
|
||||||
"@vvo/tzdb": "^6.117.0",
|
"@vvo/tzdb": "^6.117.0",
|
||||||
"csv-parse": "^5.6.0",
|
"csv-parse": "^5.6.0",
|
||||||
"inngest": "^3.19.13",
|
"inngest": "^3.19.13",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.12.2-rc.3",
|
"version": "1.12.2-rc.4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"dev": "turbo run dev --filter=@documenso/remix",
|
"dev": "turbo run dev --filter=@documenso/remix",
|
||||||
|
|||||||
@ -144,10 +144,11 @@ test('[TEMPLATES]: use template', async ({ page }) => {
|
|||||||
await page.getByRole('button', { name: 'Use Template' }).click();
|
await page.getByRole('button', { name: 'Use Template' }).click();
|
||||||
|
|
||||||
// Enter template values.
|
// Enter template values.
|
||||||
await page.getByPlaceholder('recipient.1@documenso.com').click();
|
// Get input with Email label placeholder.
|
||||||
await page.getByPlaceholder('recipient.1@documenso.com').fill(teamMemberUser.email);
|
await page.getByLabel('Email').click();
|
||||||
await page.getByPlaceholder('Recipient 1').click();
|
await page.getByLabel('Email').fill(teamMemberUser.email);
|
||||||
await page.getByPlaceholder('Recipient 1').fill('name');
|
await page.getByLabel('Name').click();
|
||||||
|
await page.getByLabel('Name').fill('name');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Create as draft' }).click();
|
await page.getByRole('button', { name: 'Create as draft' }).click();
|
||||||
await page.waitForURL(/\/t\/.+\/documents/);
|
await page.waitForURL(/\/t\/.+\/documents/);
|
||||||
|
|||||||
@ -33,6 +33,7 @@
|
|||||||
"@pdf-lib/fontkit": "^1.1.1",
|
"@pdf-lib/fontkit": "^1.1.1",
|
||||||
"@scure/base": "^1.1.3",
|
"@scure/base": "^1.1.3",
|
||||||
"@sindresorhus/slugify": "^2.2.1",
|
"@sindresorhus/slugify": "^2.2.1",
|
||||||
|
"@team-plain/typescript-sdk": "^5.9.0",
|
||||||
"@vvo/tzdb": "^6.117.0",
|
"@vvo/tzdb": "^6.117.0",
|
||||||
"csv-parse": "^5.6.0",
|
"csv-parse": "^5.6.0",
|
||||||
"inngest": "^3.19.13",
|
"inngest": "^3.19.13",
|
||||||
|
|||||||
7
packages/lib/plain/client.ts
Normal file
7
packages/lib/plain/client.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { PlainClient } from '@team-plain/typescript-sdk';
|
||||||
|
|
||||||
|
import { env } from '@documenso/lib/utils/env';
|
||||||
|
|
||||||
|
export const plainClient = new PlainClient({
|
||||||
|
apiKey: env('NEXT_PRIVATE_PLAIN_API_KEY') ?? '',
|
||||||
|
});
|
||||||
@ -111,7 +111,7 @@ export const getDocumentWhereInput = async ({
|
|||||||
visibility: {
|
visibility: {
|
||||||
in: teamVisibilityFilters,
|
in: teamVisibilityFilters,
|
||||||
},
|
},
|
||||||
teamId,
|
teamId: team.id,
|
||||||
},
|
},
|
||||||
// Or, if they are a recipient of the document.
|
// Or, if they are a recipient of the document.
|
||||||
{
|
{
|
||||||
|
|||||||
@ -134,6 +134,9 @@ export const setDocumentRecipients = async ({
|
|||||||
existingRecipient.id === recipient.id || existingRecipient.email === recipient.email,
|
existingRecipient.id === recipient.id || existingRecipient.email === recipient.email,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const canPersistedRecipientBeModified =
|
||||||
|
existing && canRecipientBeModified(existing, document.fields);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
existing &&
|
existing &&
|
||||||
hasRecipientBeenChanged(existing, recipient) &&
|
hasRecipientBeenChanged(existing, recipient) &&
|
||||||
@ -147,6 +150,7 @@ export const setDocumentRecipients = async ({
|
|||||||
return {
|
return {
|
||||||
...recipient,
|
...recipient,
|
||||||
_persisted: existing,
|
_persisted: existing,
|
||||||
|
canPersistedRecipientBeModified,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -162,6 +166,13 @@ export const setDocumentRecipients = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (recipient._persisted && !recipient.canPersistedRecipientBeModified) {
|
||||||
|
return {
|
||||||
|
...recipient._persisted,
|
||||||
|
clientId: recipient.clientId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const upsertedRecipient = await tx.recipient.upsert({
|
const upsertedRecipient = await tx.recipient.upsert({
|
||||||
where: {
|
where: {
|
||||||
id: recipient._persisted?.id ?? -1,
|
id: recipient._persisted?.id ?? -1,
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import type { Recipient } from '@prisma/client';
|
|
||||||
import { RecipientRole } from '@prisma/client';
|
import { RecipientRole } from '@prisma/client';
|
||||||
import { SendStatus, SigningStatus } from '@prisma/client';
|
import { SendStatus, SigningStatus } from '@prisma/client';
|
||||||
import { isDeepEqual } from 'remeda';
|
|
||||||
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
||||||
@ -104,10 +102,7 @@ export const updateDocumentRecipients = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!canRecipientBeModified(originalRecipient, document.fields)) {
|
||||||
hasRecipientBeenChanged(originalRecipient, recipient) &&
|
|
||||||
!canRecipientBeModified(originalRecipient, document.fields)
|
|
||||||
) {
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||||
message: 'Cannot modify a recipient who has already interacted with the document',
|
message: 'Cannot modify a recipient who has already interacted with the document',
|
||||||
});
|
});
|
||||||
@ -203,9 +198,6 @@ export const updateDocumentRecipients = async ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* If you change this you MUST update the `hasRecipientBeenChanged` function.
|
|
||||||
*/
|
|
||||||
type RecipientData = {
|
type RecipientData = {
|
||||||
id: number;
|
id: number;
|
||||||
email?: string;
|
email?: string;
|
||||||
@ -215,19 +207,3 @@ type RecipientData = {
|
|||||||
accessAuth?: TRecipientAccessAuthTypes[];
|
accessAuth?: TRecipientAccessAuthTypes[];
|
||||||
actionAuth?: TRecipientActionAuthTypes[];
|
actionAuth?: TRecipientActionAuthTypes[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
|
|
||||||
const authOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
|
||||||
|
|
||||||
const newRecipientAccessAuth = newRecipientData.accessAuth || null;
|
|
||||||
const newRecipientActionAuth = newRecipientData.actionAuth || null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
recipient.email !== newRecipientData.email ||
|
|
||||||
recipient.name !== newRecipientData.name ||
|
|
||||||
recipient.role !== newRecipientData.role ||
|
|
||||||
recipient.signingOrder !== newRecipientData.signingOrder ||
|
|
||||||
!isDeepEqual(authOptions.accessAuth, newRecipientAccessAuth) ||
|
|
||||||
!isDeepEqual(authOptions.actionAuth, newRecipientActionAuth)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
72
packages/lib/server-only/user/submit-support-ticket.ts
Normal file
72
packages/lib/server-only/user/submit-support-ticket.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { plainClient } from '@documenso/lib/plain/client';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
|
import { buildOrganisationWhereQuery } from '../../utils/organisations';
|
||||||
|
import { getTeamById } from '../team/get-team';
|
||||||
|
|
||||||
|
type SubmitSupportTicketOptions = {
|
||||||
|
subject: string;
|
||||||
|
message: string;
|
||||||
|
userId: number;
|
||||||
|
organisationId: string;
|
||||||
|
teamId?: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const submitSupportTicket = async ({
|
||||||
|
subject,
|
||||||
|
message,
|
||||||
|
userId,
|
||||||
|
organisationId,
|
||||||
|
teamId,
|
||||||
|
}: SubmitSupportTicketOptions) => {
|
||||||
|
const user = await prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||||
|
message: 'User not found',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const organisation = await prisma.organisation.findFirst({
|
||||||
|
where: buildOrganisationWhereQuery({
|
||||||
|
organisationId,
|
||||||
|
userId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!organisation) {
|
||||||
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||||
|
message: 'Organisation not found',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const team = teamId
|
||||||
|
? await getTeamById({
|
||||||
|
userId,
|
||||||
|
teamId,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const customMessage = `
|
||||||
|
Organisation: ${organisation.name} (${organisation.id})
|
||||||
|
Team: ${team ? `${team.name} (${team.id})` : 'No team provided'}
|
||||||
|
|
||||||
|
${message}`;
|
||||||
|
|
||||||
|
const res = await plainClient.createThread({
|
||||||
|
title: subject,
|
||||||
|
customerIdentifier: { emailAddress: user.email },
|
||||||
|
components: [{ componentText: { text: customMessage } }],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
throw new Error(res.error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,50 @@
|
|||||||
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
import { adminProcedure } from '../trpc';
|
||||||
|
import {
|
||||||
|
ZResetTwoFactorRequestSchema,
|
||||||
|
ZResetTwoFactorResponseSchema,
|
||||||
|
} from './reset-two-factor-authentication.types';
|
||||||
|
|
||||||
|
export const resetTwoFactorRoute = adminProcedure
|
||||||
|
.input(ZResetTwoFactorRequestSchema)
|
||||||
|
.output(ZResetTwoFactorResponseSchema)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const { userId } = input;
|
||||||
|
|
||||||
|
ctx.logger.info({
|
||||||
|
input: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return await resetTwoFactor({ userId });
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ResetTwoFactorOptions = {
|
||||||
|
userId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resetTwoFactor = async ({ userId }: ResetTwoFactorOptions) => {
|
||||||
|
const user = await prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.user.update({
|
||||||
|
where: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
twoFactorEnabled: false,
|
||||||
|
twoFactorBackupCodes: null,
|
||||||
|
twoFactorSecret: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const ZResetTwoFactorRequestSchema = z.object({
|
||||||
|
userId: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ZResetTwoFactorResponseSchema = z.void();
|
||||||
|
|
||||||
|
export type TResetTwoFactorRequest = z.infer<typeof ZResetTwoFactorRequestSchema>;
|
||||||
|
export type TResetTwoFactorResponse = z.infer<typeof ZResetTwoFactorResponseSchema>;
|
||||||
@ -21,6 +21,7 @@ import { deleteSubscriptionClaimRoute } from './delete-subscription-claim';
|
|||||||
import { findAdminOrganisationsRoute } from './find-admin-organisations';
|
import { findAdminOrganisationsRoute } from './find-admin-organisations';
|
||||||
import { findSubscriptionClaimsRoute } from './find-subscription-claims';
|
import { findSubscriptionClaimsRoute } from './find-subscription-claims';
|
||||||
import { getAdminOrganisationRoute } from './get-admin-organisation';
|
import { getAdminOrganisationRoute } from './get-admin-organisation';
|
||||||
|
import { resetTwoFactorRoute } from './reset-two-factor-authentication';
|
||||||
import {
|
import {
|
||||||
ZAdminDeleteDocumentMutationSchema,
|
ZAdminDeleteDocumentMutationSchema,
|
||||||
ZAdminDeleteUserMutationSchema,
|
ZAdminDeleteUserMutationSchema,
|
||||||
@ -51,6 +52,9 @@ export const adminRouter = router({
|
|||||||
stripe: {
|
stripe: {
|
||||||
createCustomer: createStripeCustomerRoute,
|
createCustomer: createStripeCustomerRoute,
|
||||||
},
|
},
|
||||||
|
user: {
|
||||||
|
resetTwoFactor: resetTwoFactorRoute,
|
||||||
|
},
|
||||||
|
|
||||||
// Todo: migrate old routes
|
// Todo: migrate old routes
|
||||||
findDocuments: adminProcedure.input(ZAdminFindDocumentsQuerySchema).query(async ({ input }) => {
|
findDocuments: adminProcedure.input(ZAdminFindDocumentsQuerySchema).query(async ({ input }) => {
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import type { SetAvatarImageOptions } from '@documenso/lib/server-only/profile/set-avatar-image';
|
import type { SetAvatarImageOptions } from '@documenso/lib/server-only/profile/set-avatar-image';
|
||||||
import { setAvatarImage } from '@documenso/lib/server-only/profile/set-avatar-image';
|
import { setAvatarImage } from '@documenso/lib/server-only/profile/set-avatar-image';
|
||||||
import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
|
import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
|
||||||
import { findUserSecurityAuditLogs } from '@documenso/lib/server-only/user/find-user-security-audit-logs';
|
import { findUserSecurityAuditLogs } from '@documenso/lib/server-only/user/find-user-security-audit-logs';
|
||||||
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
|
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
|
||||||
|
import { submitSupportTicket } from '@documenso/lib/server-only/user/submit-support-ticket';
|
||||||
import { updateProfile } from '@documenso/lib/server-only/user/update-profile';
|
import { updateProfile } from '@documenso/lib/server-only/user/update-profile';
|
||||||
|
|
||||||
import { adminProcedure, authenticatedProcedure, router } from '../trpc';
|
import { adminProcedure, authenticatedProcedure, router } from '../trpc';
|
||||||
@ -10,6 +12,7 @@ import {
|
|||||||
ZFindUserSecurityAuditLogsSchema,
|
ZFindUserSecurityAuditLogsSchema,
|
||||||
ZRetrieveUserByIdQuerySchema,
|
ZRetrieveUserByIdQuerySchema,
|
||||||
ZSetProfileImageMutationSchema,
|
ZSetProfileImageMutationSchema,
|
||||||
|
ZSubmitSupportTicketMutationSchema,
|
||||||
ZUpdateProfileMutationSchema,
|
ZUpdateProfileMutationSchema,
|
||||||
} from './schema';
|
} from './schema';
|
||||||
|
|
||||||
@ -91,4 +94,28 @@ export const profileRouter = router({
|
|||||||
requestMetadata: ctx.metadata,
|
requestMetadata: ctx.metadata,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
submitSupportTicket: authenticatedProcedure
|
||||||
|
.input(ZSubmitSupportTicketMutationSchema)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const { subject, message, organisationId, teamId } = input;
|
||||||
|
|
||||||
|
const userId = ctx.user.id;
|
||||||
|
|
||||||
|
const parsedTeamId = teamId ? Number(teamId) : null;
|
||||||
|
|
||||||
|
if (Number.isNaN(parsedTeamId)) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||||
|
message: 'Invalid team ID provided',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return await submitSupportTicket({
|
||||||
|
subject,
|
||||||
|
message,
|
||||||
|
userId,
|
||||||
|
organisationId,
|
||||||
|
teamId: parsedTeamId,
|
||||||
|
});
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -27,3 +27,12 @@ export const ZSetProfileImageMutationSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type TSetProfileImageMutationSchema = z.infer<typeof ZSetProfileImageMutationSchema>;
|
export type TSetProfileImageMutationSchema = z.infer<typeof ZSetProfileImageMutationSchema>;
|
||||||
|
|
||||||
|
export const ZSubmitSupportTicketMutationSchema = z.object({
|
||||||
|
organisationId: z.string(),
|
||||||
|
teamId: z.string().min(1).nullish(),
|
||||||
|
subject: z.string().min(3, 'Subject is required'),
|
||||||
|
message: z.string().min(10, 'Message must be at least 10 characters'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSupportTicketRequest = z.infer<typeof ZSubmitSupportTicketMutationSchema>;
|
||||||
|
|||||||
@ -141,6 +141,7 @@ export const authenticatedMiddleware = t.middleware(async ({ ctx, next, path })
|
|||||||
return await next({
|
return await next({
|
||||||
ctx: {
|
ctx: {
|
||||||
...ctx,
|
...ctx,
|
||||||
|
teamId: ctx.teamId || -1,
|
||||||
logger: trpcSessionLogger,
|
logger: trpcSessionLogger,
|
||||||
user: ctx.user,
|
user: ctx.user,
|
||||||
session: ctx.session,
|
session: ctx.session,
|
||||||
|
|||||||
@ -47,6 +47,7 @@
|
|||||||
"NEXT_PUBLIC_POSTHOG_KEY",
|
"NEXT_PUBLIC_POSTHOG_KEY",
|
||||||
"NEXT_PUBLIC_FEATURE_BILLING_ENABLED",
|
"NEXT_PUBLIC_FEATURE_BILLING_ENABLED",
|
||||||
"NEXT_PUBLIC_DISABLE_SIGNUP",
|
"NEXT_PUBLIC_DISABLE_SIGNUP",
|
||||||
|
"NEXT_PRIVATE_PLAIN_API_KEY",
|
||||||
"NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT",
|
"NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT",
|
||||||
"NEXT_PRIVATE_DATABASE_URL",
|
"NEXT_PRIVATE_DATABASE_URL",
|
||||||
"NEXT_PRIVATE_DIRECT_DATABASE_URL",
|
"NEXT_PRIVATE_DIRECT_DATABASE_URL",
|
||||||
|
|||||||
Reference in New Issue
Block a user