Compare commits

..

4 Commits

Author SHA1 Message Date
Mythie
5f4972d63b v1.7.0-rc.3 2024-09-03 09:27:51 +10:00
Catalin Pit
4c13176c52 feat: update createFields api endpoint (#1311)
Allow users to add 1 or more fields to a document via the /document/<id>/fields API Endpoint.
2024-09-02 21:16:48 +10:00
Mythie
d599ab0630 v1.7.0-rc.2 2024-08-29 11:01:21 +10:00
Ephraim Duncan
9e714d607e feat: disable 2fa with backup codes (#1314)
Allow disabling two-factor authentication (2FA) by using either their
authenticator app (TOTP) or a backup code.
2024-08-29 11:00:57 +10:00
15 changed files with 329 additions and 162 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "@documenso/marketing", "name": "@documenso/marketing",
"version": "1.7.0-rc.1", "version": "1.7.0-rc.3",
"private": true, "private": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@documenso/web", "name": "@documenso/web",
"version": "1.7.0-rc.1", "version": "1.7.0-rc.3",
"private": true, "private": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {

View File

@@ -15,7 +15,6 @@ import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
Dialog, Dialog,
DialogClose,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogFooter, DialogFooter,
@@ -28,13 +27,16 @@ import {
FormControl, FormControl,
FormField, FormField,
FormItem, FormItem,
FormLabel,
FormMessage, FormMessage,
} from '@documenso/ui/primitives/form/form'; } from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { PinInput, PinInputGroup, PinInputSlot } from '@documenso/ui/primitives/pin-input'; import { PinInput, PinInputGroup, PinInputSlot } from '@documenso/ui/primitives/pin-input';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export const ZDisable2FAForm = z.object({ export const ZDisable2FAForm = z.object({
token: z.string(), totpCode: z.string().trim().optional(),
backupCode: z.string().trim().optional(),
}); });
export type TDisable2FAForm = z.infer<typeof ZDisable2FAForm>; export type TDisable2FAForm = z.infer<typeof ZDisable2FAForm>;
@@ -46,21 +48,43 @@ export const DisableAuthenticatorAppDialog = () => {
const { toast } = useToast(); const { toast } = useToast();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [twoFactorDisableMethod, setTwoFactorDisableMethod] = useState<'totp' | 'backup'>('totp');
const { mutateAsync: disable2FA } = trpc.twoFactorAuthentication.disable.useMutation(); const { mutateAsync: disable2FA } = trpc.twoFactorAuthentication.disable.useMutation();
const disable2FAForm = useForm<TDisable2FAForm>({ const disable2FAForm = useForm<TDisable2FAForm>({
defaultValues: { defaultValues: {
token: '', totpCode: '',
backupCode: '',
}, },
resolver: zodResolver(ZDisable2FAForm), resolver: zodResolver(ZDisable2FAForm),
}); });
const onCloseTwoFactorDisableDialog = () => {
disable2FAForm.reset();
setIsOpen(!isOpen);
};
const onToggleTwoFactorDisableMethodClick = () => {
const method = twoFactorDisableMethod === 'totp' ? 'backup' : 'totp';
if (method === 'totp') {
disable2FAForm.setValue('backupCode', '');
}
if (method === 'backup') {
disable2FAForm.setValue('totpCode', '');
}
setTwoFactorDisableMethod(method);
};
const { isSubmitting: isDisable2FASubmitting } = disable2FAForm.formState; const { isSubmitting: isDisable2FASubmitting } = disable2FAForm.formState;
const onDisable2FAFormSubmit = async ({ token }: TDisable2FAForm) => { const onDisable2FAFormSubmit = async ({ totpCode, backupCode }: TDisable2FAForm) => {
try { try {
await disable2FA({ token }); await disable2FA({ totpCode, backupCode });
toast({ toast({
title: _(msg`Two-factor authentication disabled`), title: _(msg`Two-factor authentication disabled`),
@@ -70,7 +94,7 @@ export const DisableAuthenticatorAppDialog = () => {
}); });
flushSync(() => { flushSync(() => {
setIsOpen(false); onCloseTwoFactorDisableDialog();
}); });
router.refresh(); router.refresh();
@@ -86,7 +110,7 @@ export const DisableAuthenticatorAppDialog = () => {
}; };
return ( return (
<Dialog open={isOpen} onOpenChange={setIsOpen}> <Dialog open={isOpen} onOpenChange={onCloseTwoFactorDisableDialog}>
<DialogTrigger asChild={true}> <DialogTrigger asChild={true}>
<Button className="flex-shrink-0" variant="destructive"> <Button className="flex-shrink-0" variant="destructive">
<Trans>Disable 2FA</Trans> <Trans>Disable 2FA</Trans>
@@ -110,33 +134,59 @@ export const DisableAuthenticatorAppDialog = () => {
<Form {...disable2FAForm}> <Form {...disable2FAForm}>
<form onSubmit={disable2FAForm.handleSubmit(onDisable2FAFormSubmit)}> <form onSubmit={disable2FAForm.handleSubmit(onDisable2FAFormSubmit)}>
<fieldset className="flex flex-col gap-y-4" disabled={isDisable2FASubmitting}> <fieldset className="flex flex-col gap-y-4" disabled={isDisable2FASubmitting}>
<FormField {twoFactorDisableMethod === 'totp' && (
name="token" <FormField
control={disable2FAForm.control} name="totpCode"
render={({ field }) => ( control={disable2FAForm.control}
<FormItem> render={({ field }) => (
<FormControl> <FormItem>
<PinInput {...field} value={field.value ?? ''} maxLength={6}> <FormControl>
{Array(6) <PinInput {...field} value={field.value ?? ''} maxLength={6}>
.fill(null) {Array(6)
.map((_, i) => ( .fill(null)
<PinInputGroup key={i}> .map((_, i) => (
<PinInputSlot index={i} /> <PinInputGroup key={i}>
</PinInputGroup> <PinInputSlot index={i} />
))} </PinInputGroup>
</PinInput> ))}
</FormControl> </PinInput>
<FormMessage /> </FormControl>
</FormItem> <FormMessage />
)} </FormItem>
/> )}
/>
)}
{twoFactorDisableMethod === 'backup' && (
<FormField
control={disable2FAForm.control}
name="backupCode"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Backup Code</Trans>
</FormLabel>
<FormControl>
<Input type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<DialogFooter> <DialogFooter>
<DialogClose asChild> <Button
<Button type="button" variant="secondary"> type="button"
<Trans>Cancel</Trans> variant="secondary"
</Button> onClick={onToggleTwoFactorDisableMethodClick}
</DialogClose> >
{twoFactorDisableMethod === 'totp' ? (
<Trans>Use Backup Code</Trans>
) : (
<Trans>Use Authenticator</Trans>
)}
</Button>
<Button type="submit" variant="destructive" loading={isDisable2FASubmitting}> <Button type="submit" variant="destructive" loading={isDisable2FASubmitting}>
<Trans>Disable 2FA</Trans> <Trans>Disable 2FA</Trans>

8
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "@documenso/root", "name": "@documenso/root",
"version": "1.7.0-rc.1", "version": "1.7.0-rc.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@documenso/root", "name": "@documenso/root",
"version": "1.7.0-rc.1", "version": "1.7.0-rc.3",
"workspaces": [ "workspaces": [
"apps/*", "apps/*",
"packages/*" "packages/*"
@@ -81,7 +81,7 @@
}, },
"apps/marketing": { "apps/marketing": {
"name": "@documenso/marketing", "name": "@documenso/marketing",
"version": "1.7.0-rc.1", "version": "1.7.0-rc.3",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@documenso/assets": "*", "@documenso/assets": "*",
@@ -424,7 +424,7 @@
}, },
"apps/web": { "apps/web": {
"name": "@documenso/web", "name": "@documenso/web",
"version": "1.7.0-rc.1", "version": "1.7.0-rc.3",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@documenso/api": "*", "@documenso/api": "*",

View File

@@ -1,6 +1,6 @@
{ {
"private": true, "private": true,
"version": "1.7.0-rc.1", "version": "1.7.0-rc.3",
"scripts": { "scripts": {
"build": "turbo run build", "build": "turbo run build",
"build:web": "turbo run build --filter=@documenso/web", "build:web": "turbo run build --filter=@documenso/web",

View File

@@ -21,6 +21,7 @@ import {
ZSendDocumentForSigningMutationSchema, ZSendDocumentForSigningMutationSchema,
ZSuccessfulDeleteTemplateResponseSchema, ZSuccessfulDeleteTemplateResponseSchema,
ZSuccessfulDocumentResponseSchema, ZSuccessfulDocumentResponseSchema,
ZSuccessfulFieldCreationResponseSchema,
ZSuccessfulFieldResponseSchema, ZSuccessfulFieldResponseSchema,
ZSuccessfulGetDocumentResponseSchema, ZSuccessfulGetDocumentResponseSchema,
ZSuccessfulGetTemplateResponseSchema, ZSuccessfulGetTemplateResponseSchema,
@@ -236,7 +237,7 @@ export const ApiContractV1 = c.router(
path: '/api/v1/documents/:id/fields', path: '/api/v1/documents/:id/fields',
body: ZCreateFieldMutationSchema, body: ZCreateFieldMutationSchema,
responses: { responses: {
200: ZSuccessfulFieldResponseSchema, 200: ZSuccessfulFieldCreationResponseSchema,
400: ZUnsuccessfulResponseSchema, 400: ZUnsuccessfulResponseSchema,
401: ZUnsuccessfulResponseSchema, 401: ZUnsuccessfulResponseSchema,
404: ZUnsuccessfulResponseSchema, 404: ZUnsuccessfulResponseSchema,

View File

@@ -1,4 +1,5 @@
import { createNextRoute } from '@ts-rest/next'; import { createNextRoute } from '@ts-rest/next';
import { match } from 'ts-pattern';
import { getServerLimits } from '@documenso/ee/server-only/limits/server'; import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
@@ -15,7 +16,6 @@ import { getDocumentById } from '@documenso/lib/server-only/document/get-documen
import { resendDocument } from '@documenso/lib/server-only/document/resend-document'; import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
import { sendDocument } from '@documenso/lib/server-only/document/send-document'; import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { updateDocument } from '@documenso/lib/server-only/document/update-document'; import { updateDocument } from '@documenso/lib/server-only/document/update-document';
import { createField } from '@documenso/lib/server-only/field/create-field';
import { deleteField } from '@documenso/lib/server-only/field/delete-field'; import { deleteField } from '@documenso/lib/server-only/field/delete-field';
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id'; import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
import { updateField } from '@documenso/lib/server-only/field/update-field'; import { updateField } from '@documenso/lib/server-only/field/update-field';
@@ -32,6 +32,13 @@ import { deleteTemplate } from '@documenso/lib/server-only/template/delete-templ
import { findTemplates } from '@documenso/lib/server-only/template/find-templates'; import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id'; import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta'; import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import {
ZCheckboxFieldMeta,
ZDropdownFieldMeta,
ZNumberFieldMeta,
ZRadioFieldMeta,
ZTextFieldMeta,
} from '@documenso/lib/types/field-meta';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { getFile } from '@documenso/lib/universal/upload/get-file'; import { getFile } from '@documenso/lib/universal/upload/get-file';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
@@ -39,6 +46,8 @@ import {
getPresignGetUrl, getPresignGetUrl,
getPresignPostUrl, getPresignPostUrl,
} from '@documenso/lib/universal/upload/server-actions'; } from '@documenso/lib/universal/upload/server-actions';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import { DocumentDataType, DocumentStatus, SigningStatus } from '@documenso/prisma/client'; import { DocumentDataType, DocumentStatus, SigningStatus } from '@documenso/prisma/client';
import { ApiContractV1 } from './contract'; import { ApiContractV1 } from './contract';
@@ -870,100 +879,167 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
createField: authenticatedMiddleware(async (args, user, team) => { createField: authenticatedMiddleware(async (args, user, team) => {
const { id: documentId } = args.params; const { id: documentId } = args.params;
const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY, fieldMeta } = const fields = Array.isArray(args.body) ? args.body : [args.body];
args.body;
if (pageNumber <= 0) { const document = await prisma.document.findFirst({
return { select: { id: true, status: true },
status: 400, where: {
body: { id: Number(documentId),
message: 'Invalid page number', ...(team?.id
}, ? {
}; team: {
} id: team.id,
members: { some: { userId: user.id } },
const document = await getDocumentById({ },
id: Number(documentId), }
userId: user.id, : {
teamId: team?.id, userId: user.id,
teamId: null,
}),
},
}); });
if (!document) { if (!document) {
return { return {
status: 404, status: 404,
body: { body: { message: 'Document not found' },
message: 'Document not found',
},
}; };
} }
if (document.status === DocumentStatus.COMPLETED) { if (document.status === DocumentStatus.COMPLETED) {
return { return {
status: 400, status: 400,
body: { body: { message: 'Document is already completed' },
message: 'Document is already completed',
},
};
}
const recipient = await getRecipientById({
id: Number(recipientId),
documentId: Number(documentId),
}).catch(() => null);
if (!recipient) {
return {
status: 404,
body: {
message: 'Recipient not found',
},
};
}
if (recipient.signingStatus === SigningStatus.SIGNED) {
return {
status: 400,
body: {
message: 'Recipient has already signed the document',
},
}; };
} }
try { try {
const field = await createField({ const createdFields = await prisma.$transaction(async (tx) => {
documentId: Number(documentId), return Promise.all(
recipientId: Number(recipientId), fields.map(async (fieldData) => {
userId: user.id, const {
teamId: team?.id, recipientId,
type, type,
pageNumber, pageNumber,
pageX, pageWidth,
pageY, pageHeight,
pageWidth, pageX,
pageHeight, pageY,
fieldMeta, fieldMeta,
requestMetadata: extractNextApiRequestMetadata(args.req), } = fieldData;
});
const remappedField = { if (pageNumber <= 0) {
id: field.id, throw new Error('Invalid page number');
documentId: field.documentId, }
recipientId: field.recipientId ?? -1,
type: field.type, const recipient = await getRecipientById({
pageNumber: field.page, id: Number(recipientId),
pageX: Number(field.positionX), documentId: Number(documentId),
pageY: Number(field.positionY), }).catch(() => null);
pageWidth: Number(field.width),
pageHeight: Number(field.height), if (!recipient) {
customText: field.customText, throw new Error('Recipient not found');
fieldMeta: ZFieldMetaSchema.parse(field.fieldMeta), }
inserted: field.inserted,
}; if (recipient.signingStatus === SigningStatus.SIGNED) {
throw new Error('Recipient has already signed the document');
}
const advancedField = ['NUMBER', 'RADIO', 'CHECKBOX', 'DROPDOWN', 'TEXT'].includes(
type,
);
if (advancedField && !fieldMeta) {
throw new Error(
'Field meta is required for this type of field. Please provide the appropriate field meta object.',
);
}
if (fieldMeta && fieldMeta.type.toLowerCase() !== String(type).toLowerCase()) {
throw new Error('Field meta type does not match the field type');
}
const result = match(type)
.with('RADIO', () => ZRadioFieldMeta.safeParse(fieldMeta))
.with('CHECKBOX', () => ZCheckboxFieldMeta.safeParse(fieldMeta))
.with('DROPDOWN', () => ZDropdownFieldMeta.safeParse(fieldMeta))
.with('NUMBER', () => ZNumberFieldMeta.safeParse(fieldMeta))
.with('TEXT', () => ZTextFieldMeta.safeParse(fieldMeta))
.with('SIGNATURE', 'INITIALS', 'DATE', 'EMAIL', 'NAME', () => ({
success: true,
data: {},
}))
.with('FREE_SIGNATURE', () => ({
success: false,
error: 'FREE_SIGNATURE is not supported',
data: {},
}))
.exhaustive();
if (!result.success) {
throw new Error('Field meta parsing failed');
}
const field = await tx.field.create({
data: {
documentId: Number(documentId),
recipientId: Number(recipientId),
type,
page: pageNumber,
positionX: pageX,
positionY: pageY,
width: pageWidth,
height: pageHeight,
customText: '',
inserted: false,
fieldMeta: result.data,
},
include: {
Recipient: true,
},
});
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: 'FIELD_CREATED',
documentId: Number(documentId),
user: {
id: team?.id ?? user.id,
email: team?.name ?? user.email,
name: team ? '' : user.name,
},
data: {
fieldId: field.secondaryId,
fieldRecipientEmail: field.Recipient?.email ?? '',
fieldRecipientId: recipientId,
fieldType: field.type,
},
requestMetadata: extractNextApiRequestMetadata(args.req),
}),
});
return {
id: field.id,
documentId: Number(field.documentId),
recipientId: field.recipientId ?? -1,
type: field.type,
pageNumber: field.page,
pageX: Number(field.positionX),
pageY: Number(field.positionY),
pageWidth: Number(field.width),
pageHeight: Number(field.height),
customText: field.customText,
fieldMeta: ZFieldMetaSchema.parse(field.fieldMeta),
inserted: field.inserted,
};
}),
);
});
return { return {
status: 200, status: 200,
body: { body: {
...remappedField, fields: createdFields,
documentId: Number(documentId), documentId: Number(documentId),
}, },
}; };

View File

@@ -293,7 +293,7 @@ export type TSuccessfulRecipientResponseSchema = z.infer<typeof ZSuccessfulRecip
/** /**
* Fields * Fields
*/ */
export const ZCreateFieldMutationSchema = z.object({ const ZCreateFieldSchema = z.object({
recipientId: z.number(), recipientId: z.number(),
type: z.nativeEnum(FieldType), type: z.nativeEnum(FieldType),
pageNumber: z.number(), pageNumber: z.number(),
@@ -301,12 +301,17 @@ export const ZCreateFieldMutationSchema = z.object({
pageY: z.number(), pageY: z.number(),
pageWidth: z.number(), pageWidth: z.number(),
pageHeight: z.number(), pageHeight: z.number(),
fieldMeta: ZFieldMetaSchema, fieldMeta: ZFieldMetaSchema.openapi({}),
}); });
export const ZCreateFieldMutationSchema = z.union([
ZCreateFieldSchema,
z.array(ZCreateFieldSchema).min(1),
]);
export type TCreateFieldMutationSchema = z.infer<typeof ZCreateFieldMutationSchema>; export type TCreateFieldMutationSchema = z.infer<typeof ZCreateFieldMutationSchema>;
export const ZUpdateFieldMutationSchema = ZCreateFieldMutationSchema.partial(); export const ZUpdateFieldMutationSchema = ZCreateFieldSchema.partial();
export type TUpdateFieldMutationSchema = z.infer<typeof ZUpdateFieldMutationSchema>; export type TUpdateFieldMutationSchema = z.infer<typeof ZUpdateFieldMutationSchema>;
@@ -314,6 +319,26 @@ export const ZDeleteFieldMutationSchema = null;
export type TDeleteFieldMutationSchema = typeof ZDeleteFieldMutationSchema; export type TDeleteFieldMutationSchema = typeof ZDeleteFieldMutationSchema;
const ZSuccessfulFieldSchema = z.object({
id: z.number(),
documentId: z.number(),
recipientId: z.number(),
type: z.nativeEnum(FieldType),
pageNumber: z.number(),
pageX: z.number(),
pageY: z.number(),
pageWidth: z.number(),
pageHeight: z.number(),
customText: z.string(),
fieldMeta: ZFieldMetaSchema,
inserted: z.boolean(),
});
export const ZSuccessfulFieldCreationResponseSchema = z.object({
fields: z.union([ZSuccessfulFieldSchema, z.array(ZSuccessfulFieldSchema)]),
documentId: z.number(),
});
export const ZSuccessfulFieldResponseSchema = z.object({ export const ZSuccessfulFieldResponseSchema = z.object({
id: z.number(), id: z.number(),
documentId: z.number(), documentId: z.number(),

View File

@@ -2,25 +2,33 @@ import { prisma } from '@documenso/prisma';
import type { User } from '@documenso/prisma/client'; import type { User } from '@documenso/prisma/client';
import { UserSecurityAuditLogType } from '@documenso/prisma/client'; import { UserSecurityAuditLogType } from '@documenso/prisma/client';
import { AppError } from '../../errors/app-error'; import { AppError, AppErrorCode } from '../../errors/app-error';
import type { RequestMetadata } from '../../universal/extract-request-metadata'; import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { validateTwoFactorAuthentication } from './validate-2fa'; import { validateTwoFactorAuthentication } from './validate-2fa';
type DisableTwoFactorAuthenticationOptions = { type DisableTwoFactorAuthenticationOptions = {
user: User; user: User;
token: string; totpCode?: string;
backupCode?: string;
requestMetadata?: RequestMetadata; requestMetadata?: RequestMetadata;
}; };
export const disableTwoFactorAuthentication = async ({ export const disableTwoFactorAuthentication = async ({
token, totpCode,
backupCode,
user, user,
requestMetadata, requestMetadata,
}: DisableTwoFactorAuthenticationOptions) => { }: DisableTwoFactorAuthenticationOptions) => {
let isValid = await validateTwoFactorAuthentication({ totpCode: token, user }); let isValid = false;
if (!isValid) { if (!totpCode && !backupCode) {
isValid = await validateTwoFactorAuthentication({ backupCode: token, user }); throw new AppError(AppErrorCode.INVALID_REQUEST);
}
if (totpCode) {
isValid = await validateTwoFactorAuthentication({ totpCode, user });
} else if (backupCode) {
isValid = await validateTwoFactorAuthentication({ backupCode, user });
} }
if (!isValid) { if (!isValid) {

View File

@@ -110,24 +110,21 @@ export const createField = async ({
} }
const result = match(type) const result = match(type)
.with('RADIO', () => { .with('RADIO', () => ZRadioFieldMeta.safeParse(fieldMeta))
return ZRadioFieldMeta.safeParse(fieldMeta); .with('CHECKBOX', () => ZCheckboxFieldMeta.safeParse(fieldMeta))
}) .with('DROPDOWN', () => ZDropdownFieldMeta.safeParse(fieldMeta))
.with('CHECKBOX', () => { .with('NUMBER', () => ZNumberFieldMeta.safeParse(fieldMeta))
return ZCheckboxFieldMeta.safeParse(fieldMeta); .with('TEXT', () => ZTextFieldMeta.safeParse(fieldMeta))
}) .with('SIGNATURE', 'INITIALS', 'DATE', 'EMAIL', 'NAME', () => ({
.with('DROPDOWN', () => { success: true,
return ZDropdownFieldMeta.safeParse(fieldMeta); data: {},
}) }))
.with('NUMBER', () => { .with('FREE_SIGNATURE', () => ({
return ZNumberFieldMeta.safeParse(fieldMeta); success: false,
}) error: 'FREE_SIGNATURE is not supported',
.with('TEXT', () => { data: {},
return ZTextFieldMeta.safeParse(fieldMeta); }))
}) .exhaustive();
.otherwise(() => {
return { success: false, data: {} };
});
if (!result.success) { if (!result.success) {
throw new Error('Field meta parsing failed'); throw new Error('Field meta parsing failed');
@@ -145,7 +142,7 @@ export const createField = async ({
height: pageHeight, height: pageHeight,
customText: '', customText: '',
inserted: false, inserted: false,
fieldMeta: advancedField ? result.data : undefined, fieldMeta: result.data,
}, },
include: { include: {
Recipient: true, Recipient: true,

View File

@@ -37,6 +37,10 @@ export const updateField = async ({
requestMetadata, requestMetadata,
fieldMeta, fieldMeta,
}: UpdateFieldOptions) => { }: UpdateFieldOptions) => {
if (type === 'FREE_SIGNATURE') {
throw new Error('Cannot update a FREE_SIGNATURE field');
}
const oldField = await prisma.field.findFirstOrThrow({ const oldField = await prisma.field.findFirstOrThrow({
where: { where: {
id: fieldId, id: fieldId,

View File

@@ -607,6 +607,7 @@ msgstr ""
msgid "Background Color" msgid "Background Color"
msgstr "" msgstr ""
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:167
#: apps/web/src/components/forms/signin.tsx:451 #: apps/web/src/components/forms/signin.tsx:451
msgid "Backup Code" msgid "Backup Code"
msgstr "" msgstr ""
@@ -684,7 +685,6 @@ msgstr ""
#: apps/web/src/components/(teams)/dialogs/transfer-team-dialog.tsx:278 #: apps/web/src/components/(teams)/dialogs/transfer-team-dialog.tsx:278
#: apps/web/src/components/(teams)/dialogs/update-team-email-dialog.tsx:162 #: apps/web/src/components/(teams)/dialogs/update-team-email-dialog.tsx:162
#: apps/web/src/components/(teams)/dialogs/update-team-member-dialog.tsx:187 #: apps/web/src/components/(teams)/dialogs/update-team-member-dialog.tsx:187
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:137
#: apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx:257 #: apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx:257
#: apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx:163 #: apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx:163
#: apps/web/src/components/templates/manage-public-template-dialog.tsx:452 #: apps/web/src/components/templates/manage-public-template-dialog.tsx:452
@@ -1143,9 +1143,9 @@ msgstr ""
msgid "Disable" msgid "Disable"
msgstr "" msgstr ""
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:92 #: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:116
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:99 #: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:123
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:142 #: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:192
msgid "Disable 2FA" msgid "Disable 2FA"
msgstr "" msgstr ""
@@ -2279,7 +2279,7 @@ msgstr ""
msgid "Please note that you will lose access to all documents associated with this team & all the members will be removed and notified" msgid "Please note that you will lose access to all documents associated with this team & all the members will be removed and notified"
msgstr "" msgstr ""
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:103 #: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:127
msgid "Please provide a token from the authenticator, or a backup code. If you do not have a backup code available, please contact support." msgid "Please provide a token from the authenticator, or a backup code. If you do not have a backup code available, please contact support."
msgstr "" msgstr ""
@@ -3453,7 +3453,7 @@ msgstr ""
msgid "Two-Factor Authentication" msgid "Two-Factor Authentication"
msgstr "" msgstr ""
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:66 #: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:90
msgid "Two-factor authentication disabled" msgid "Two-factor authentication disabled"
msgstr "" msgstr ""
@@ -3461,7 +3461,7 @@ msgstr ""
msgid "Two-factor authentication enabled" msgid "Two-factor authentication enabled"
msgstr "" msgstr ""
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:68 #: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:92
msgid "Two-factor authentication has been disabled for your account. You will no longer be required to enter a code from your authenticator app when signing in." msgid "Two-factor authentication has been disabled for your account. You will no longer be required to enter a code from your authenticator app when signing in."
msgstr "" msgstr ""
@@ -3506,7 +3506,7 @@ msgstr ""
msgid "Unable to delete team" msgid "Unable to delete team"
msgstr "" msgstr ""
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:79 #: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:103
msgid "Unable to disable two-factor authentication" msgid "Unable to disable two-factor authentication"
msgstr "" msgstr ""
@@ -3654,10 +3654,12 @@ msgstr ""
msgid "Uploaded file not an allowed file type" msgid "Uploaded file not an allowed file type"
msgstr "" msgstr ""
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:187
#: apps/web/src/components/forms/signin.tsx:471 #: apps/web/src/components/forms/signin.tsx:471
msgid "Use Authenticator" msgid "Use Authenticator"
msgstr "" msgstr ""
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:185
#: apps/web/src/components/forms/signin.tsx:469 #: apps/web/src/components/forms/signin.tsx:469
msgid "Use Backup Code" msgid "Use Backup Code"
msgstr "" msgstr ""
@@ -3944,7 +3946,7 @@ msgstr ""
msgid "We were unable to create a checkout session. Please try again, or contact support" msgid "We were unable to create a checkout session. Please try again, or contact support"
msgstr "" msgstr ""
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:81 #: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:105
msgid "We were unable to disable two-factor authentication for your account. Please ensure that you have entered your password and backup code correctly and try again." msgid "We were unable to disable two-factor authentication for your account. Please ensure that you have entered your password and backup code correctly and try again."
msgstr "" msgstr ""

View File

@@ -602,6 +602,7 @@ msgstr "Back to Documents"
msgid "Background Color" msgid "Background Color"
msgstr "Background Color" msgstr "Background Color"
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:167
#: apps/web/src/components/forms/signin.tsx:451 #: apps/web/src/components/forms/signin.tsx:451
msgid "Backup Code" msgid "Backup Code"
msgstr "Backup Code" msgstr "Backup Code"
@@ -679,7 +680,6 @@ msgstr "By enabling 2FA, you will be required to enter a code from your authenti
#: apps/web/src/components/(teams)/dialogs/transfer-team-dialog.tsx:278 #: apps/web/src/components/(teams)/dialogs/transfer-team-dialog.tsx:278
#: apps/web/src/components/(teams)/dialogs/update-team-email-dialog.tsx:162 #: apps/web/src/components/(teams)/dialogs/update-team-email-dialog.tsx:162
#: apps/web/src/components/(teams)/dialogs/update-team-member-dialog.tsx:187 #: apps/web/src/components/(teams)/dialogs/update-team-member-dialog.tsx:187
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:137
#: apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx:257 #: apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx:257
#: apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx:163 #: apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx:163
#: apps/web/src/components/templates/manage-public-template-dialog.tsx:452 #: apps/web/src/components/templates/manage-public-template-dialog.tsx:452
@@ -1138,9 +1138,9 @@ msgstr "Direct template link usage exceeded ({0}/{1})"
msgid "Disable" msgid "Disable"
msgstr "Disable" msgstr "Disable"
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:92 #: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:116
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:99 #: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:123
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:142 #: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:192
msgid "Disable 2FA" msgid "Disable 2FA"
msgstr "Disable 2FA" msgstr "Disable 2FA"
@@ -2274,7 +2274,7 @@ msgstr "Please note that this action is irreversible. Once confirmed, your webho
msgid "Please note that you will lose access to all documents associated with this team & all the members will be removed and notified" msgid "Please note that you will lose access to all documents associated with this team & all the members will be removed and notified"
msgstr "Please note that you will lose access to all documents associated with this team & all the members will be removed and notified" msgstr "Please note that you will lose access to all documents associated with this team & all the members will be removed and notified"
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:103 #: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:127
msgid "Please provide a token from the authenticator, or a backup code. If you do not have a backup code available, please contact support." msgid "Please provide a token from the authenticator, or a backup code. If you do not have a backup code available, please contact support."
msgstr "Please provide a token from the authenticator, or a backup code. If you do not have a backup code available, please contact support." msgstr "Please provide a token from the authenticator, or a backup code. If you do not have a backup code available, please contact support."
@@ -3448,7 +3448,7 @@ msgstr "Two factor authentication recovery codes are used to access your account
msgid "Two-Factor Authentication" msgid "Two-Factor Authentication"
msgstr "Two-Factor Authentication" msgstr "Two-Factor Authentication"
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:66 #: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:90
msgid "Two-factor authentication disabled" msgid "Two-factor authentication disabled"
msgstr "Two-factor authentication disabled" msgstr "Two-factor authentication disabled"
@@ -3456,7 +3456,7 @@ msgstr "Two-factor authentication disabled"
msgid "Two-factor authentication enabled" msgid "Two-factor authentication enabled"
msgstr "Two-factor authentication enabled" msgstr "Two-factor authentication enabled"
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:68 #: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:92
msgid "Two-factor authentication has been disabled for your account. You will no longer be required to enter a code from your authenticator app when signing in." msgid "Two-factor authentication has been disabled for your account. You will no longer be required to enter a code from your authenticator app when signing in."
msgstr "Two-factor authentication has been disabled for your account. You will no longer be required to enter a code from your authenticator app when signing in." msgstr "Two-factor authentication has been disabled for your account. You will no longer be required to enter a code from your authenticator app when signing in."
@@ -3501,7 +3501,7 @@ msgstr "Unable to delete invitation. Please try again."
msgid "Unable to delete team" msgid "Unable to delete team"
msgstr "Unable to delete team" msgstr "Unable to delete team"
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:79 #: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:103
msgid "Unable to disable two-factor authentication" msgid "Unable to disable two-factor authentication"
msgstr "Unable to disable two-factor authentication" msgstr "Unable to disable two-factor authentication"
@@ -3649,10 +3649,12 @@ msgstr "Uploaded file is too small"
msgid "Uploaded file not an allowed file type" msgid "Uploaded file not an allowed file type"
msgstr "Uploaded file not an allowed file type" msgstr "Uploaded file not an allowed file type"
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:187
#: apps/web/src/components/forms/signin.tsx:471 #: apps/web/src/components/forms/signin.tsx:471
msgid "Use Authenticator" msgid "Use Authenticator"
msgstr "Use Authenticator" msgstr "Use Authenticator"
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:185
#: apps/web/src/components/forms/signin.tsx:469 #: apps/web/src/components/forms/signin.tsx:469
msgid "Use Backup Code" msgid "Use Backup Code"
msgstr "Use Backup Code" msgstr "Use Backup Code"
@@ -3939,7 +3941,7 @@ msgstr "We were unable to copy your recovery code to your clipboard. Please try
msgid "We were unable to create a checkout session. Please try again, or contact support" msgid "We were unable to create a checkout session. Please try again, or contact support"
msgstr "We were unable to create a checkout session. Please try again, or contact support" msgstr "We were unable to create a checkout session. Please try again, or contact support"
#: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:81 #: apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx:105
msgid "We were unable to disable two-factor authentication for your account. Please ensure that you have entered your password and backup code correctly and try again." msgid "We were unable to disable two-factor authentication for your account. Please ensure that you have entered your password and backup code correctly and try again."
msgstr "We were unable to disable two-factor authentication for your account. Please ensure that you have entered your password and backup code correctly and try again." msgstr "We were unable to disable two-factor authentication for your account. Please ensure that you have entered your password and backup code correctly and try again."

View File

@@ -65,7 +65,8 @@ export const twoFactorAuthenticationRouter = router({
return await disableTwoFactorAuthentication({ return await disableTwoFactorAuthentication({
user, user,
token: input.token, totpCode: input.totpCode,
backupCode: input.backupCode,
requestMetadata: extractNextApiRequestMetadata(ctx.req), requestMetadata: extractNextApiRequestMetadata(ctx.req),
}); });
} catch (err) { } catch (err) {

View File

@@ -9,7 +9,8 @@ export type TEnableTwoFactorAuthenticationMutationSchema = z.infer<
>; >;
export const ZDisableTwoFactorAuthenticationMutationSchema = z.object({ export const ZDisableTwoFactorAuthenticationMutationSchema = z.object({
token: z.string().trim().min(1), totpCode: z.string().trim().optional(),
backupCode: z.string().trim().optional(),
}); });
export type TDisableTwoFactorAuthenticationMutationSchema = z.infer< export type TDisableTwoFactorAuthenticationMutationSchema = z.infer<