chore: review

This commit is contained in:
Ephraim Atta-Duncan
2025-08-19 10:20:12 +00:00
parent 262d9efdd5
commit d6bc4bd0ba
8 changed files with 81 additions and 37 deletions

View File

@ -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) {

View File

@ -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.

View File

@ -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({

View File

@ -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,
]), ]),
); );

View File

@ -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 {

View File

@ -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');
}; };

View File

@ -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();
}
};

View File

@ -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>
)} )}