mirror of
https://github.com/documenso/documenso.git
synced 2026-07-26 09:54:51 +10:00
Merge branch 'main' into fix/cc-recipient-order-last
This commit is contained in:
@@ -23,6 +23,9 @@ export const DOCUMENT_STATUS: {
|
||||
[DocumentStatus.REJECTED]: {
|
||||
description: msg`Rejected`,
|
||||
},
|
||||
[DocumentStatus.CANCELLED]: {
|
||||
description: msg`Cancelled`,
|
||||
},
|
||||
[DocumentStatus.DRAFT]: {
|
||||
description: msg`Draft`,
|
||||
},
|
||||
|
||||
@@ -27,6 +27,7 @@ export enum AppErrorCode {
|
||||
ENVELOPE_DRAFT = 'ENVELOPE_DRAFT',
|
||||
ENVELOPE_COMPLETED = 'ENVELOPE_COMPLETED',
|
||||
ENVELOPE_REJECTED = 'ENVELOPE_REJECTED',
|
||||
ENVELOPE_CANCELLED = 'ENVELOPE_CANCELLED',
|
||||
ENVELOPE_LEGACY = 'ENVELOPE_LEGACY',
|
||||
/**
|
||||
* Authoring mutation rejected because the envelope is an AES/QES envelope
|
||||
@@ -80,6 +81,7 @@ export const genericErrorCodeToTrpcErrorCodeMap: Record<string, { code: string;
|
||||
[AppErrorCode.ENVELOPE_DRAFT]: { code: 'BAD_REQUEST', status: 400 },
|
||||
[AppErrorCode.ENVELOPE_COMPLETED]: { code: 'BAD_REQUEST', status: 400 },
|
||||
[AppErrorCode.ENVELOPE_REJECTED]: { code: 'BAD_REQUEST', status: 400 },
|
||||
[AppErrorCode.ENVELOPE_CANCELLED]: { code: 'BAD_REQUEST', status: 400 },
|
||||
[AppErrorCode.ENVELOPE_LEGACY]: { code: 'BAD_REQUEST', status: 400 },
|
||||
[AppErrorCode.ENVELOPE_TSP_LOCKED]: { code: 'BAD_REQUEST', status: 400 },
|
||||
[AppErrorCode.CSC_INSTANCE_MODE_MISMATCH]: { code: 'BAD_REQUEST', status: 400 },
|
||||
@@ -286,6 +288,7 @@ export class AppError extends Error {
|
||||
AppErrorCode.ENVELOPE_DRAFT,
|
||||
AppErrorCode.ENVELOPE_COMPLETED,
|
||||
AppErrorCode.ENVELOPE_REJECTED,
|
||||
AppErrorCode.ENVELOPE_CANCELLED,
|
||||
AppErrorCode.ENVELOPE_LEGACY,
|
||||
AppErrorCode.ENVELOPE_TSP_LOCKED,
|
||||
AppErrorCode.CSC_INSTANCE_MODE_MISMATCH,
|
||||
|
||||
@@ -18,6 +18,7 @@ export const getDocumentStats = async () => {
|
||||
[ExtendedDocumentStatus.PENDING]: 0,
|
||||
[ExtendedDocumentStatus.COMPLETED]: 0,
|
||||
[ExtendedDocumentStatus.REJECTED]: 0,
|
||||
[ExtendedDocumentStatus.CANCELLED]: 0,
|
||||
[ExtendedDocumentStatus.ALL]: 0,
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { jobs } from '@documenso/lib/jobs/client';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, EnvelopeType, WebhookTriggerEvents } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } from '../../types/webhook-payload';
|
||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { mapSecondaryIdToDocumentId, unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||
import { isMemberManagerOrAbove } from '../../utils/teams';
|
||||
import { getMemberRoles } from '../team/get-member-roles';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
export type CancelDocumentOptions = {
|
||||
id: EnvelopeIdOptions;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
reason?: string;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
export const cancelDocument = async ({ id, userId, teamId, reason, requestMetadata }: CancelDocumentOptions) => {
|
||||
// Note: This is an unsafe request, we validate the ownership/permission later in the function.
|
||||
const envelope = await prisma.envelope.findUnique({
|
||||
where: unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT),
|
||||
include: {
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const isUserOwner = envelope.userId === userId;
|
||||
|
||||
const teamRole = await getMemberRoles({
|
||||
teamId: envelope.teamId,
|
||||
reference: {
|
||||
type: 'User',
|
||||
id: userId,
|
||||
},
|
||||
})
|
||||
.then((roles) => roles.teamRole)
|
||||
.catch(() => null);
|
||||
|
||||
const isUserTeamMember = teamRole !== null;
|
||||
|
||||
// Callers with no relationship to the document must not be able to determine
|
||||
// whether it exists, so respond as if it was not found.
|
||||
if (!isUserOwner && !isUserTeamMember) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const isPrivilegedTeamMember = teamRole && isMemberManagerOrAbove(teamRole);
|
||||
|
||||
// The document is visible to the caller, but cancelling requires elevated permissions.
|
||||
if (!isUserOwner && !isPrivilegedTeamMember) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'Not allowed',
|
||||
});
|
||||
}
|
||||
|
||||
if (envelope.status !== DocumentStatus.PENDING) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Only pending documents can be cancelled',
|
||||
});
|
||||
}
|
||||
|
||||
const updatedEnvelope = await prisma.$transaction(async (tx) => {
|
||||
const updated = await tx.envelope.update({
|
||||
where: {
|
||||
id: envelope.id,
|
||||
},
|
||||
data: {
|
||||
status: DocumentStatus.CANCELLED,
|
||||
completedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
envelopeId: envelope.id,
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CANCELLED,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
reason,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return updated;
|
||||
});
|
||||
|
||||
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
||||
|
||||
// Send cancellation emails to recipients via the resilient background job.
|
||||
await jobs.triggerJob({
|
||||
name: 'send.document.cancelled.emails',
|
||||
payload: {
|
||||
documentId: legacyDocumentId,
|
||||
cancellationReason: reason,
|
||||
requestMetadata: requestMetadata.requestMetadata,
|
||||
},
|
||||
});
|
||||
|
||||
// Trigger the webhook with the updated (cancelled) envelope payload.
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_CANCELLED,
|
||||
data: ZWebhookDocumentSchema.parse(
|
||||
mapEnvelopeToWebhookDocumentPayload({
|
||||
...envelope,
|
||||
status: updatedEnvelope.status,
|
||||
completedAt: updatedEnvelope.completedAt,
|
||||
}),
|
||||
),
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
return updatedEnvelope;
|
||||
};
|
||||
@@ -220,7 +220,11 @@ export const findDocuments = async ({
|
||||
eb.or([
|
||||
eb('Envelope.userId', '=', user.id),
|
||||
eb.and([
|
||||
eb('Envelope.status', 'in', [sql.lit(DocumentStatus.COMPLETED), sql.lit(DocumentStatus.PENDING)]),
|
||||
eb('Envelope.status', 'in', [
|
||||
sql.lit(DocumentStatus.COMPLETED),
|
||||
sql.lit(DocumentStatus.PENDING),
|
||||
sql.lit(DocumentStatus.CANCELLED),
|
||||
]),
|
||||
recipientExists(eb, user.email),
|
||||
]),
|
||||
]),
|
||||
@@ -291,6 +295,16 @@ export const findDocuments = async ({
|
||||
]),
|
||||
),
|
||||
)
|
||||
.with(ExtendedDocumentStatus.CANCELLED, () =>
|
||||
qb
|
||||
.where('Envelope.status', '=', sql.lit(ExtendedDocumentStatus.CANCELLED))
|
||||
.where((eb) =>
|
||||
eb.and([
|
||||
personalDeletedFilter(eb),
|
||||
eb.or([eb('Envelope.userId', '=', user.id), recipientExists(eb, user.email)]),
|
||||
]),
|
||||
),
|
||||
)
|
||||
.exhaustive();
|
||||
};
|
||||
|
||||
@@ -429,6 +443,18 @@ export const findDocuments = async ({
|
||||
return eb.and([teamDeletedFilter(eb), visibilityFilter(eb), eb.or(accessBranches)]);
|
||||
}),
|
||||
)
|
||||
.with(ExtendedDocumentStatus.CANCELLED, () =>
|
||||
qb.where('Envelope.status', '=', sql.lit(ExtendedDocumentStatus.CANCELLED)).where((eb) => {
|
||||
const accessBranches = [eb('Envelope.teamId', '=', teamData.id)];
|
||||
|
||||
if (teamEmail) {
|
||||
accessBranches.push(senderEmailIs(eb, teamEmail));
|
||||
accessBranches.push(recipientExists(eb, teamEmail));
|
||||
}
|
||||
|
||||
return eb.and([teamDeletedFilter(eb), visibilityFilter(eb), eb.or(accessBranches)]);
|
||||
}),
|
||||
)
|
||||
.exhaustive();
|
||||
};
|
||||
|
||||
|
||||
@@ -239,6 +239,20 @@ export const getStats = async ({ userId, teamId, period, search = '', folderId,
|
||||
return eb.and([teamDeletedFilter(eb), visibilityFilter(eb), eb.or(accessBranches)]);
|
||||
});
|
||||
|
||||
// CANCELLED: team-owned cancelled + team-email received cancelled docs
|
||||
const cancelledQuery = buildBaseQuery()
|
||||
.where('Envelope.status', '=', sql.lit(DocumentStatus.CANCELLED))
|
||||
.where((eb) => {
|
||||
const accessBranches = [eb('Envelope.teamId', '=', team.id)];
|
||||
|
||||
if (teamEmail) {
|
||||
accessBranches.push(senderEmailIs(eb, teamEmail));
|
||||
accessBranches.push(recipientExists(eb, teamEmail));
|
||||
}
|
||||
|
||||
return eb.and([teamDeletedFilter(eb), visibilityFilter(eb), eb.or(accessBranches)]);
|
||||
});
|
||||
|
||||
// INBOX: non-draft docs where team email is a NOT_SIGNED, non-CC recipient
|
||||
// Returns 0 if the team has no team email.
|
||||
const inboxQuery = teamEmail
|
||||
@@ -260,21 +274,23 @@ export const getStats = async ({ userId, teamId, period, search = '', folderId,
|
||||
|
||||
// ─── Execute all counts in parallel ──────────────────────────────────
|
||||
|
||||
const [draft, pending, completed, rejected, inbox] = await Promise.all([
|
||||
const [draft, pending, completed, rejected, cancelled, inbox] = await Promise.all([
|
||||
cappedCount(draftQuery),
|
||||
cappedCount(pendingQuery),
|
||||
cappedCount(completedQuery),
|
||||
cappedCount(rejectedQuery),
|
||||
cappedCount(cancelledQuery),
|
||||
inboxQuery ? cappedCount(inboxQuery) : Promise.resolve(0),
|
||||
]);
|
||||
|
||||
const all = Math.min(draft + pending + completed + rejected + inbox, STATS_COUNT_CAP);
|
||||
const all = Math.min(draft + pending + completed + rejected + cancelled + inbox, STATS_COUNT_CAP);
|
||||
|
||||
const stats: Record<ExtendedDocumentStatus, number> = {
|
||||
[ExtendedDocumentStatus.DRAFT]: draft,
|
||||
[ExtendedDocumentStatus.PENDING]: pending,
|
||||
[ExtendedDocumentStatus.COMPLETED]: completed,
|
||||
[ExtendedDocumentStatus.REJECTED]: rejected,
|
||||
[ExtendedDocumentStatus.CANCELLED]: cancelled,
|
||||
[ExtendedDocumentStatus.INBOX]: inbox,
|
||||
[ExtendedDocumentStatus.ALL]: all,
|
||||
};
|
||||
|
||||
@@ -34,8 +34,9 @@ type EnvelopeIdRef = Pick<Envelope, 'id'>;
|
||||
* Throws:
|
||||
* - `ENVELOPE_TSP_LOCKED` when the envelope is PENDING (the case unique to
|
||||
* the TSP lock — SES routes happily allow PENDING).
|
||||
* - `ENVELOPE_COMPLETED` / `ENVELOPE_REJECTED` for those terminal states, to
|
||||
* stay consistent with the existing envelope-state error vocabulary.
|
||||
* - `ENVELOPE_COMPLETED` / `ENVELOPE_REJECTED` / `ENVELOPE_CANCELLED` for those
|
||||
* terminal states, to stay consistent with the existing envelope-state error
|
||||
* vocabulary.
|
||||
*/
|
||||
export function assertEnvelopeMutable(envelope: EnvelopeMutableSnapshot): Promise<void>;
|
||||
export function assertEnvelopeMutable(envelope: EnvelopeIdRef, tx: Prisma.TransactionClient): Promise<void>;
|
||||
@@ -73,6 +74,7 @@ const assertSnapshotMutable = (envelope: EnvelopeMutableSnapshot): void => {
|
||||
.with(DocumentStatus.PENDING, () => AppErrorCode.ENVELOPE_TSP_LOCKED)
|
||||
.with(DocumentStatus.COMPLETED, () => AppErrorCode.ENVELOPE_COMPLETED)
|
||||
.with(DocumentStatus.REJECTED, () => AppErrorCode.ENVELOPE_REJECTED)
|
||||
.with(DocumentStatus.CANCELLED, () => AppErrorCode.ENVELOPE_CANCELLED)
|
||||
.otherwise(() => AppErrorCode.INVALID_REQUEST);
|
||||
|
||||
throw new AppError(errorCode, {
|
||||
|
||||
@@ -17,6 +17,7 @@ export const generateSampleWebhookPayload = (event: WebhookTriggerEvents, webhoo
|
||||
const now = new Date();
|
||||
const basePayload = {
|
||||
id: 10,
|
||||
envelopeId: 'env_123',
|
||||
externalId: null,
|
||||
userId: 1,
|
||||
authOptions: null,
|
||||
@@ -52,6 +53,7 @@ export const generateSampleWebhookPayload = (event: WebhookTriggerEvents, webhoo
|
||||
recipients: [
|
||||
{
|
||||
id: 52,
|
||||
envelopeId: 'env_123',
|
||||
documentId: 10,
|
||||
templateId: null,
|
||||
email: 'signer@documenso.com',
|
||||
@@ -73,6 +75,7 @@ export const generateSampleWebhookPayload = (event: WebhookTriggerEvents, webhoo
|
||||
Recipient: [
|
||||
{
|
||||
id: 52,
|
||||
envelopeId: 'env_123',
|
||||
documentId: 10,
|
||||
templateId: null,
|
||||
email: 'signer@documenso.com',
|
||||
@@ -269,6 +272,7 @@ export const generateSampleWebhookPayload = (event: WebhookTriggerEvents, webhoo
|
||||
recipients: [
|
||||
{
|
||||
id: 50,
|
||||
envelopeId: 'env_123',
|
||||
documentId: 10,
|
||||
templateId: null,
|
||||
email: 'signer2@documenso.com',
|
||||
@@ -291,6 +295,7 @@ export const generateSampleWebhookPayload = (event: WebhookTriggerEvents, webhoo
|
||||
},
|
||||
{
|
||||
id: 51,
|
||||
envelopeId: 'env_123',
|
||||
documentId: 10,
|
||||
templateId: null,
|
||||
email: 'signer1@documenso.com',
|
||||
@@ -315,6 +320,7 @@ export const generateSampleWebhookPayload = (event: WebhookTriggerEvents, webhoo
|
||||
Recipient: [
|
||||
{
|
||||
id: 50,
|
||||
envelopeId: 'env_123',
|
||||
documentId: 10,
|
||||
templateId: null,
|
||||
email: 'signer2@documenso.com',
|
||||
@@ -337,6 +343,7 @@ export const generateSampleWebhookPayload = (event: WebhookTriggerEvents, webhoo
|
||||
},
|
||||
{
|
||||
id: 51,
|
||||
envelopeId: 'env_123',
|
||||
documentId: 10,
|
||||
templateId: null,
|
||||
email: 'signer1@documenso.com',
|
||||
@@ -444,6 +451,7 @@ export const generateSampleWebhookPayload = (event: WebhookTriggerEvents, webhoo
|
||||
recipients: [
|
||||
{
|
||||
id: 7,
|
||||
envelopeId: 'env_123',
|
||||
documentId: 7,
|
||||
templateId: null,
|
||||
email: 'signer1@documenso.com',
|
||||
@@ -468,6 +476,7 @@ export const generateSampleWebhookPayload = (event: WebhookTriggerEvents, webhoo
|
||||
Recipient: [
|
||||
{
|
||||
id: 7,
|
||||
envelopeId: 'env_123',
|
||||
documentId: 7,
|
||||
templateId: null,
|
||||
email: 'signer@documenso.com',
|
||||
|
||||
@@ -31,6 +31,7 @@ export const ZDocumentAuditLogTypeSchema = z.enum([
|
||||
'DOCUMENT_COMPLETED', // When the document is sealed and fully completed.
|
||||
'DOCUMENT_CREATED', // When the document is created.
|
||||
'DOCUMENT_DELETED', // When the document is soft deleted.
|
||||
'DOCUMENT_CANCELLED', // When a privileged member cancels the document.
|
||||
'DOCUMENT_FIELDS_AUTO_INSERTED', // When a field is auto inserted during send due to default values (radio/dropdown/checkbox).
|
||||
'DOCUMENT_FIELD_INSERTED', // When a field is inserted (signed/approved/etc) by a recipient.
|
||||
'DOCUMENT_FIELD_UNINSERTED', // When a field is uninserted by a recipient.
|
||||
@@ -296,6 +297,16 @@ export const ZDocumentAuditLogEventDocumentDeletedSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Event: Document cancelled.
|
||||
*/
|
||||
export const ZDocumentAuditLogEventDocumentCancelledSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CANCELLED),
|
||||
data: z.object({
|
||||
reason: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Event: Document field inserted.
|
||||
*/
|
||||
@@ -815,6 +826,7 @@ export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
|
||||
ZDocumentAuditLogEventDocumentCompletedSchema,
|
||||
ZDocumentAuditLogEventDocumentCreatedSchema,
|
||||
ZDocumentAuditLogEventDocumentDeletedSchema,
|
||||
ZDocumentAuditLogEventDocumentCancelledSchema,
|
||||
ZDocumentAuditLogEventDocumentMovedToTeamSchema,
|
||||
ZDocumentAuditLogEventDocumentDelegatedOwnerCreatedSchema,
|
||||
ZDocumentAuditLogEventDocumentFieldsAutoInsertedSchema,
|
||||
|
||||
@@ -21,6 +21,7 @@ import { mapSecondaryIdToDocumentId, mapSecondaryIdToTemplateId } from '../utils
|
||||
*/
|
||||
export const ZWebhookRecipientSchema = z.object({
|
||||
id: z.number(),
|
||||
envelopeId: z.string(),
|
||||
documentId: z.number().nullable(),
|
||||
templateId: z.number().nullable(),
|
||||
email: z.string(),
|
||||
@@ -64,6 +65,7 @@ export const ZWebhookDocumentMetaSchema = z.object({
|
||||
*/
|
||||
export const ZWebhookDocumentSchema = z.object({
|
||||
id: z.number(),
|
||||
envelopeId: z.string(),
|
||||
externalId: z.string().nullable(),
|
||||
userId: z.number(),
|
||||
authOptions: z.any().nullable(),
|
||||
@@ -117,6 +119,7 @@ export const mapEnvelopeToWebhookDocumentPayload = (
|
||||
|
||||
const mappedRecipients = rawRecipients.map((recipient) => ({
|
||||
id: recipient.id,
|
||||
envelopeId: envelope.id,
|
||||
documentId: envelope.type === EnvelopeType.DOCUMENT ? legacyId : null,
|
||||
templateId: envelope.type === EnvelopeType.TEMPLATE ? legacyId : null,
|
||||
email: recipient.email,
|
||||
@@ -137,6 +140,7 @@ export const mapEnvelopeToWebhookDocumentPayload = (
|
||||
|
||||
return {
|
||||
id: legacyId,
|
||||
envelopeId: envelope.id,
|
||||
externalId: envelope.externalId,
|
||||
userId: envelope.userId,
|
||||
authOptions: envelope.authOptions,
|
||||
|
||||
@@ -13,7 +13,27 @@ type File = {
|
||||
arrayBuffer: () => Promise<ArrayBuffer>;
|
||||
};
|
||||
|
||||
export const putPdfFile = async (file: File) => {
|
||||
/**
|
||||
* Options for uploads that are not authorized by a logged-in session.
|
||||
*
|
||||
* Embedded authoring flows run cross-origin without a session cookie, so they
|
||||
* must authorize uploads with their embedding presign token instead.
|
||||
*/
|
||||
export type PutFileOptions = {
|
||||
presignToken?: string;
|
||||
};
|
||||
|
||||
const buildUploadAuthHeaders = (options?: PutFileOptions): Record<string, string> => {
|
||||
if (!options?.presignToken) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${options.presignToken}`,
|
||||
};
|
||||
};
|
||||
|
||||
export const putPdfFile = async (file: File, options?: PutFileOptions) => {
|
||||
const formData = new FormData();
|
||||
|
||||
// Create a proper File object from the data
|
||||
@@ -25,6 +45,7 @@ export const putPdfFile = async (file: File) => {
|
||||
|
||||
const response = await fetch('/api/files/upload-pdf', {
|
||||
method: 'POST',
|
||||
headers: buildUploadAuthHeaders(options),
|
||||
body: formData,
|
||||
});
|
||||
|
||||
@@ -41,12 +62,12 @@ export const putPdfFile = async (file: File) => {
|
||||
/**
|
||||
* Uploads a file to the appropriate storage location.
|
||||
*/
|
||||
export const putFile = async (file: File) => {
|
||||
export const putFile = async (file: File, options?: PutFileOptions) => {
|
||||
const NEXT_PUBLIC_UPLOAD_TRANSPORT = env('NEXT_PUBLIC_UPLOAD_TRANSPORT');
|
||||
|
||||
return await match(NEXT_PUBLIC_UPLOAD_TRANSPORT)
|
||||
.with('s3', async () => putFileInObjectStorage(file, {}))
|
||||
.with('azure-blob', async () => putFileInObjectStorage(file, { 'x-ms-blob-type': 'BlockBlob' }))
|
||||
.with('s3', async () => putFileInObjectStorage(file, {}, options))
|
||||
.with('azure-blob', async () => putFileInObjectStorage(file, { 'x-ms-blob-type': 'BlockBlob' }, options))
|
||||
.otherwise(async () => putFileInDatabase(file));
|
||||
};
|
||||
|
||||
@@ -63,11 +84,12 @@ const putFileInDatabase = async (file: File) => {
|
||||
};
|
||||
};
|
||||
|
||||
const putFileInObjectStorage = async (file: File, extraHeaders: Record<string, string>) => {
|
||||
const putFileInObjectStorage = async (file: File, extraHeaders: Record<string, string>, options?: PutFileOptions) => {
|
||||
const getPresignedUrlResponse = await fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/api/files/presigned-post-url`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...buildUploadAuthHeaders(options),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
fileName: file.name,
|
||||
|
||||
@@ -355,6 +355,14 @@ export const formatDocumentAuditLogAction = (i18n: I18n, auditLog: TDocumentAudi
|
||||
you: msg`You deleted the document`,
|
||||
user: msg`${user} deleted the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CANCELLED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `Document cancelled`,
|
||||
context: `Audit log format`,
|
||||
}),
|
||||
you: msg`You cancelled the document`,
|
||||
user: msg`${user} cancelled the document`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELDS_AUTO_INSERTED }, () => ({
|
||||
anonymous: msg({
|
||||
message: `System auto inserted fields`,
|
||||
|
||||
@@ -12,7 +12,9 @@ import { mapRecipientToLegacyRecipient } from './recipients';
|
||||
export const isDocumentCompleted = (document: Pick<Envelope, 'status'> | DocumentStatus) => {
|
||||
const status = typeof document === 'string' ? document : document.status;
|
||||
|
||||
return status === DocumentStatus.COMPLETED || status === DocumentStatus.REJECTED;
|
||||
return (
|
||||
status === DocumentStatus.COMPLETED || status === DocumentStatus.REJECTED || status === DocumentStatus.CANCELLED
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -242,12 +242,13 @@ export const getEnvelopeItemPermissions = (
|
||||
envelope: Pick<Envelope, 'completedAt' | 'deletedAt' | 'type' | 'status'>,
|
||||
recipients: Pick<Recipient, 'role' | 'signingStatus' | 'sendStatus'>[],
|
||||
): EnvelopeItemPermissions => {
|
||||
// Always reject completed/rejected/deleted envelopes.
|
||||
// Always reject completed/rejected/cancelled/deleted envelopes.
|
||||
if (
|
||||
envelope.completedAt ||
|
||||
envelope.deletedAt ||
|
||||
envelope.status === DocumentStatus.REJECTED ||
|
||||
envelope.status === DocumentStatus.COMPLETED
|
||||
envelope.status === DocumentStatus.COMPLETED ||
|
||||
envelope.status === DocumentStatus.CANCELLED
|
||||
) {
|
||||
return {
|
||||
canTitleBeChanged: false,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TeamGroup, TeamMemberRole } from '@documenso/prisma/generated/types';
|
||||
import { type TeamGroup, TeamMemberRole } from '@documenso/prisma/generated/types';
|
||||
import type { DocumentVisibility, OrganisationGlobalSettings, Prisma, TeamGlobalSettings } from '@prisma/client';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
|
||||
@@ -235,3 +235,11 @@ export const extractDerivedTeamSettings = (
|
||||
|
||||
return derivedSettings;
|
||||
};
|
||||
|
||||
export const isMemberManagerOrAbove = (role: TeamMemberRole) => {
|
||||
return role === TeamMemberRole.ADMIN || role === TeamMemberRole.MANAGER;
|
||||
};
|
||||
|
||||
export const isMemberAdmin = (role: TeamMemberRole) => {
|
||||
return role === TeamMemberRole.ADMIN;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user