mirror of
https://github.com/documenso/documenso.git
synced 2025-11-16 01:32:06 +10:00
chore: review
This commit is contained in:
@ -130,8 +130,10 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
|||||||
const { documentMeta } = document;
|
const { documentMeta } = document;
|
||||||
|
|
||||||
if (isRecipientExpired(recipient)) {
|
if (isRecipientExpired(recipient)) {
|
||||||
await expireRecipient({ recipientId: recipient.id });
|
const expiredRecipient = await expireRecipient({ recipientId: recipient.id });
|
||||||
throw redirect(`/sign/${token}/expired`);
|
if (expiredRecipient) {
|
||||||
|
throw redirect(`/sign/${token}/expired`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recipient.signingStatus === SigningStatus.REJECTED) {
|
if (recipient.signingStatus === SigningStatus.REJECTED) {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
||||||
import { RefObject, useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the width and height of a text element.
|
* Calculate the width and height of a text element.
|
||||||
|
|||||||
@ -201,6 +201,7 @@ export const resendDocument = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (document.documentMeta?.expiryAmount && document.documentMeta?.expiryUnit) {
|
if (document.documentMeta?.expiryAmount && document.documentMeta?.expiryUnit) {
|
||||||
|
const previousExpiryDate = recipient.expired;
|
||||||
const newExpiryDate = calculateRecipientExpiry(
|
const newExpiryDate = calculateRecipientExpiry(
|
||||||
document.documentMeta.expiryAmount,
|
document.documentMeta.expiryAmount,
|
||||||
document.documentMeta.expiryUnit,
|
document.documentMeta.expiryUnit,
|
||||||
@ -215,6 +216,21 @@ export const resendDocument = async ({
|
|||||||
expired: newExpiryDate,
|
expired: newExpiryDate,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await tx.documentAuditLog.create({
|
||||||
|
data: createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_EXPIRY_EXTENDED,
|
||||||
|
documentId: document.id,
|
||||||
|
metadata: requestMetadata,
|
||||||
|
data: {
|
||||||
|
recipientId: recipient.id,
|
||||||
|
recipientName: recipient.name,
|
||||||
|
recipientEmail: recipient.email,
|
||||||
|
previousExpiryDate,
|
||||||
|
newExpiryDate,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await tx.documentAuditLog.create({
|
await tx.documentAuditLog.create({
|
||||||
|
|||||||
@ -40,6 +40,7 @@ export const ZDocumentAuditLogTypeSchema = z.enum([
|
|||||||
'DOCUMENT_TITLE_UPDATED', // When the document title is updated.
|
'DOCUMENT_TITLE_UPDATED', // When the document title is updated.
|
||||||
'DOCUMENT_EXTERNAL_ID_UPDATED', // When the document external ID is updated.
|
'DOCUMENT_EXTERNAL_ID_UPDATED', // When the document external ID is updated.
|
||||||
'DOCUMENT_MOVED_TO_TEAM', // When the document is moved to a team.
|
'DOCUMENT_MOVED_TO_TEAM', // When the document is moved to a team.
|
||||||
|
'DOCUMENT_RECIPIENT_EXPIRY_EXTENDED', // When a recipient's expiry is extended via resend.
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const ZDocumentAuditLogEmailTypeSchema = z.enum([
|
export const ZDocumentAuditLogEmailTypeSchema = z.enum([
|
||||||
@ -598,6 +599,20 @@ export const ZDocumentAuditLogEventDocumentMovedToTeamSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event: Recipient expiry extended.
|
||||||
|
*/
|
||||||
|
export const ZDocumentAuditLogEventRecipientExpiryExtendedSchema = z.object({
|
||||||
|
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_EXPIRY_EXTENDED),
|
||||||
|
data: z.object({
|
||||||
|
recipientId: z.number(),
|
||||||
|
recipientName: z.string().optional(),
|
||||||
|
recipientEmail: z.string(),
|
||||||
|
previousExpiryDate: z.date().nullable(),
|
||||||
|
newExpiryDate: z.date().nullable(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
export const ZDocumentAuditLogBaseSchema = z.object({
|
export const ZDocumentAuditLogBaseSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
@ -636,6 +651,7 @@ export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
|
|||||||
ZDocumentAuditLogEventRecipientAddedSchema,
|
ZDocumentAuditLogEventRecipientAddedSchema,
|
||||||
ZDocumentAuditLogEventRecipientUpdatedSchema,
|
ZDocumentAuditLogEventRecipientUpdatedSchema,
|
||||||
ZDocumentAuditLogEventRecipientRemovedSchema,
|
ZDocumentAuditLogEventRecipientRemovedSchema,
|
||||||
|
ZDocumentAuditLogEventRecipientExpiryExtendedSchema,
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -492,6 +492,10 @@ export const formatDocumentAuditLogAction = (
|
|||||||
context: `Audit log format`,
|
context: `Audit log format`,
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_EXPIRY_EXTENDED }, (data) => ({
|
||||||
|
anonymous: msg`Recipient expiry extended`,
|
||||||
|
identified: msg`${prefix} extended expiry for ${data.data.recipientEmail}`,
|
||||||
|
}))
|
||||||
.exhaustive();
|
.exhaustive();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
import type { Recipient } from '@prisma/client';
|
import type { Recipient } from '@prisma/client';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
|
export interface DurationValue {
|
||||||
|
amount: number;
|
||||||
|
unit: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const calculateRecipientExpiry = (
|
export const calculateRecipientExpiry = (
|
||||||
documentExpiryAmount?: number | null,
|
documentExpiryAmount?: number | null,
|
||||||
documentExpiryUnit?: string | null,
|
documentExpiryUnit?: string | null,
|
||||||
@ -45,6 +50,23 @@ export const isValidExpirySettings = (
|
|||||||
return expiryAmount > 0 && ['minutes', 'hours', 'days', 'weeks', 'months'].includes(expiryUnit);
|
return expiryAmount > 0 && ['minutes', 'hours', 'days', 'weeks', 'months'].includes(expiryUnit);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const calculateExpiryDate = (duration: DurationValue, fromDate: Date = new Date()): Date => {
|
||||||
|
switch (duration.unit) {
|
||||||
|
case 'minutes':
|
||||||
|
return DateTime.fromJSDate(fromDate).plus({ minutes: duration.amount }).toJSDate();
|
||||||
|
case 'hours':
|
||||||
|
return DateTime.fromJSDate(fromDate).plus({ hours: duration.amount }).toJSDate();
|
||||||
|
case 'days':
|
||||||
|
return DateTime.fromJSDate(fromDate).plus({ days: duration.amount }).toJSDate();
|
||||||
|
case 'weeks':
|
||||||
|
return DateTime.fromJSDate(fromDate).plus({ weeks: duration.amount }).toJSDate();
|
||||||
|
case 'months':
|
||||||
|
return DateTime.fromJSDate(fromDate).plus({ months: duration.amount }).toJSDate();
|
||||||
|
default:
|
||||||
|
return DateTime.fromJSDate(fromDate).plus({ days: duration.amount }).toJSDate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const formatExpiryDate = (date: Date): string => {
|
export const formatExpiryDate = (date: Date): string => {
|
||||||
return DateTime.fromJSDate(date).toFormat('MMM dd, yyyy HH:mm');
|
return DateTime.fromJSDate(date).toFormat('MMM dd, yyyy HH:mm');
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,20 +2,12 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { useLingui } from '@lingui/react';
|
import type { DurationValue } from '@documenso/lib/utils/expiry';
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
|
|
||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
import { Input } from './input';
|
import { Input } from './input';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select';
|
||||||
|
|
||||||
export type TimeUnit = 'minutes' | 'hours' | 'days' | 'weeks' | 'months';
|
|
||||||
|
|
||||||
export interface DurationValue {
|
|
||||||
amount: number;
|
|
||||||
unit: TimeUnit;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DurationSelectorProps {
|
export interface DurationSelectorProps {
|
||||||
value?: DurationValue;
|
value?: DurationValue;
|
||||||
onChange?: (value: DurationValue) => void;
|
onChange?: (value: DurationValue) => void;
|
||||||
@ -25,7 +17,7 @@ export interface DurationSelectorProps {
|
|||||||
maxAmount?: number;
|
maxAmount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TIME_UNITS: Array<{ value: TimeUnit; label: string; labelPlural: string }> = [
|
const TIME_UNITS: Array<{ value: string; label: string; labelPlural: string }> = [
|
||||||
{ value: 'minutes', label: 'Minute', labelPlural: 'Minutes' },
|
{ value: 'minutes', label: 'Minute', labelPlural: 'Minutes' },
|
||||||
{ value: 'hours', label: 'Hour', labelPlural: 'Hours' },
|
{ value: 'hours', label: 'Hour', labelPlural: 'Hours' },
|
||||||
{ value: 'days', label: 'Day', labelPlural: 'Days' },
|
{ value: 'days', label: 'Day', labelPlural: 'Days' },
|
||||||
@ -41,8 +33,6 @@ export const DurationSelector = ({
|
|||||||
minAmount = 1,
|
minAmount = 1,
|
||||||
maxAmount = 365,
|
maxAmount = 365,
|
||||||
}: DurationSelectorProps) => {
|
}: DurationSelectorProps) => {
|
||||||
const { _ } = useLingui();
|
|
||||||
|
|
||||||
const handleAmountChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleAmountChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const amount = parseInt(event.target.value, 10);
|
const amount = parseInt(event.target.value, 10);
|
||||||
if (!isNaN(amount) && amount >= minAmount && amount <= maxAmount) {
|
if (!isNaN(amount) && amount >= minAmount && amount <= maxAmount) {
|
||||||
@ -50,11 +40,11 @@ export const DurationSelector = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUnitChange = (unit: TimeUnit) => {
|
const handleUnitChange = (unit: string) => {
|
||||||
onChange?.({ ...value, unit });
|
onChange?.({ ...value, unit });
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUnitLabel = (unit: TimeUnit, amount: number) => {
|
const getUnitLabel = (unit: string, amount: number) => {
|
||||||
const unitConfig = TIME_UNITS.find((u) => u.value === unit);
|
const unitConfig = TIME_UNITS.find((u) => u.value === unit);
|
||||||
if (!unitConfig) return unit;
|
if (!unitConfig) return unit;
|
||||||
|
|
||||||
@ -87,20 +77,3 @@ export const DurationSelector = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const calculateExpiryDate = (duration: DurationValue, fromDate: Date = new Date()): Date => {
|
|
||||||
switch (duration.unit) {
|
|
||||||
case 'minutes':
|
|
||||||
return DateTime.fromJSDate(fromDate).plus({ minutes: duration.amount }).toJSDate();
|
|
||||||
case 'hours':
|
|
||||||
return DateTime.fromJSDate(fromDate).plus({ hours: duration.amount }).toJSDate();
|
|
||||||
case 'days':
|
|
||||||
return DateTime.fromJSDate(fromDate).plus({ days: duration.amount }).toJSDate();
|
|
||||||
case 'weeks':
|
|
||||||
return DateTime.fromJSDate(fromDate).plus({ weeks: duration.amount }).toJSDate();
|
|
||||||
case 'months':
|
|
||||||
return DateTime.fromJSDate(fromDate).plus({ months: duration.amount }).toJSDate();
|
|
||||||
default:
|
|
||||||
return DateTime.fromJSDate(fromDate).plus({ days: duration.amount }).toJSDate();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@ -3,11 +3,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useLingui } from '@lingui/react';
|
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { calculateExpiryDate, formatExpiryDate } from '@documenso/lib/utils/expiry';
|
||||||
|
|
||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
import { DurationSelector } from './duration-selector';
|
import { DurationSelector } from './duration-selector';
|
||||||
import {
|
import {
|
||||||
@ -48,8 +49,6 @@ export const ExpirySettingsPicker = ({
|
|||||||
onValueChange,
|
onValueChange,
|
||||||
value,
|
value,
|
||||||
}: ExpirySettingsPickerProps) => {
|
}: ExpirySettingsPickerProps) => {
|
||||||
const { _ } = useLingui();
|
|
||||||
|
|
||||||
const form = useForm<ExpirySettings>({
|
const form = useForm<ExpirySettings>({
|
||||||
resolver: zodResolver(ZExpirySettingsSchema),
|
resolver: zodResolver(ZExpirySettingsSchema),
|
||||||
defaultValues,
|
defaultValues,
|
||||||
@ -59,6 +58,13 @@ export const ExpirySettingsPicker = ({
|
|||||||
const { watch, setValue, getValues } = form;
|
const { watch, setValue, getValues } = form;
|
||||||
const expiryDuration = watch('expiryDuration');
|
const expiryDuration = watch('expiryDuration');
|
||||||
|
|
||||||
|
const calculatedExpiryDate = React.useMemo(() => {
|
||||||
|
if (expiryDuration?.amount && expiryDuration?.unit) {
|
||||||
|
return calculateExpiryDate(expiryDuration);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [expiryDuration]);
|
||||||
|
|
||||||
// Call onValueChange when form values change
|
// Call onValueChange when form values change
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const subscription = watch((value) => {
|
const subscription = watch((value) => {
|
||||||
@ -111,6 +117,11 @@ export const ExpirySettingsPicker = ({
|
|||||||
maxAmount={365}
|
maxAmount={365}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
{calculatedExpiryDate && (
|
||||||
|
<FormDescription>
|
||||||
|
<Trans>Links will expire on: {formatExpiryDate(calculatedExpiryDate)}</Trans>
|
||||||
|
</FormDescription>
|
||||||
|
)}
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user