Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
import { env } from '@/lib/env';
|
|
|
|
|
import { logger } from '@/lib/logger';
|
2026-04-28 02:22:04 +02:00
|
|
|
import { getPortDocumensoConfig, type DocumensoApiVersion } from '@/lib/services/port-config';
|
fix(audit-tier-1): timeouts, lifecycle, per-port Documenso, FK constraints
Closes the second wave of HIGH-priority audit findings:
* fetchWithTimeout helper (new src/lib/fetch-with-timeout.ts) wraps
Documenso, OCR, currency, Umami, IMAP, etc. — a hung upstream can
no longer pin a worker concurrency slot indefinitely. OpenAI client
passes timeout: 30_000. ImapFlow gets socket / greeting / connection
timeouts.
* SIGTERM / SIGINT handler in src/server.ts drains in-flight HTTP,
closes Socket.io, and disconnects Redis before exit; compose
stop_grace_period bumped to 30s. Adds closeSocketServer() helper.
* env.ts gains zod-validated PORT and MULTI_NODE_DEPLOYMENT, and
filesystem.ts now reads from env (a typo can no longer silently
disable the multi-node guard).
* Per-port Documenso template + recipient IDs land in system_settings
with env fallback (PortDocumensoConfig now exposes eoiTemplateId,
clientRecipientId, developerRecipientId, approvalRecipientId).
document-templates.ts uses the per-port config and threads portId
into documensoGenerateFromTemplate().
* Migration 0042 wires the eleven HIGH-tier missing FK constraints
(documents/files/interests/reminders/berth_waiting_list/
form_submissions) plus polymorphic CHECK round 2
(yacht_ownership_history.owner_type, document_sends.document_kind),
invoices.billing_entity_id NOT EMPTY, and clients.merged_into self-FK.
Drizzle schema columns updated to .references(...) where possible
so the misleading "FK wired in relations.ts" comments are gone.
Test status: 1168/1168 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §§5,6,7,8,9,10 +
MED §§14,15,16,18.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:52:58 +02:00
|
|
|
import { fetchWithTimeout } from '@/lib/fetch-with-timeout';
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
|
feat(admin): per-port email/Documenso/branding/reminder settings + invitations
Centralizes everything operators need to configure into the admin panel,
each setting per-port with env fallback.
New admin pages
- /admin landing page linking to every admin section as a card
- /admin/email FROM name+address, reply-to, signature/footer HTML,
optional SMTP host/port/user/pass override
- /admin/documenso API URL+key override, EOI Documenso template ID,
default EOI pathway (documenso-template vs inapp),
"Test connection" button
- /admin/branding logo URL, primary color, app name, email
header/footer HTML
- /admin/reminders port-level defaults for new interests +
port-wide daily-digest delivery window
- /admin/invitations send / list / resend / revoke CRM invitations
Per-user reminder digest
- /notifications/preferences gains a Reminder digest card:
immediate / daily / weekly / off, with HH:MM, day-of-week,
IANA timezone fields. Stored in user_profiles.preferences.reminders.
Plumbing
- port-config.ts typed accessors (getPortEmailConfig, getPortDocumensoConfig,
getPortBrandingConfig, getPortReminderConfig) — settings → env fallback.
- sendEmail accepts optional portId; resolves From/SMTP from settings
when supplied.
- documensoFetch + downloadSignedPdf accept optional portId; each public
function takes it through. checkDocumensoHealth() backs the test button.
- crm-invite.service gains listCrmInvites / revokeCrmInvite / resendCrmInvite
with audit-log entries (revoke_invite, resend_invite added to AuditAction).
- AdminLandingPage card grid + shared SettingsFormCard component to remove
per-page form boilerplate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:21:54 +02:00
|
|
|
interface DocumensoCreds {
|
|
|
|
|
baseUrl: string;
|
|
|
|
|
apiKey: string;
|
2026-04-28 02:22:04 +02:00
|
|
|
apiVersion: DocumensoApiVersion;
|
feat(admin): per-port email/Documenso/branding/reminder settings + invitations
Centralizes everything operators need to configure into the admin panel,
each setting per-port with env fallback.
New admin pages
- /admin landing page linking to every admin section as a card
- /admin/email FROM name+address, reply-to, signature/footer HTML,
optional SMTP host/port/user/pass override
- /admin/documenso API URL+key override, EOI Documenso template ID,
default EOI pathway (documenso-template vs inapp),
"Test connection" button
- /admin/branding logo URL, primary color, app name, email
header/footer HTML
- /admin/reminders port-level defaults for new interests +
port-wide daily-digest delivery window
- /admin/invitations send / list / resend / revoke CRM invitations
Per-user reminder digest
- /notifications/preferences gains a Reminder digest card:
immediate / daily / weekly / off, with HH:MM, day-of-week,
IANA timezone fields. Stored in user_profiles.preferences.reminders.
Plumbing
- port-config.ts typed accessors (getPortEmailConfig, getPortDocumensoConfig,
getPortBrandingConfig, getPortReminderConfig) — settings → env fallback.
- sendEmail accepts optional portId; resolves From/SMTP from settings
when supplied.
- documensoFetch + downloadSignedPdf accept optional portId; each public
function takes it through. checkDocumensoHealth() backs the test button.
- crm-invite.service gains listCrmInvites / revokeCrmInvite / resendCrmInvite
with audit-log entries (revoke_invite, resend_invite added to AuditAction).
- AdminLandingPage card grid + shared SettingsFormCard component to remove
per-page form boilerplate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:21:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function resolveCreds(portId?: string): Promise<DocumensoCreds> {
|
2026-04-28 02:22:04 +02:00
|
|
|
if (!portId) {
|
|
|
|
|
return {
|
|
|
|
|
baseUrl: env.DOCUMENSO_API_URL,
|
|
|
|
|
apiKey: env.DOCUMENSO_API_KEY,
|
|
|
|
|
apiVersion: env.DOCUMENSO_API_VERSION,
|
|
|
|
|
};
|
|
|
|
|
}
|
feat(admin): per-port email/Documenso/branding/reminder settings + invitations
Centralizes everything operators need to configure into the admin panel,
each setting per-port with env fallback.
New admin pages
- /admin landing page linking to every admin section as a card
- /admin/email FROM name+address, reply-to, signature/footer HTML,
optional SMTP host/port/user/pass override
- /admin/documenso API URL+key override, EOI Documenso template ID,
default EOI pathway (documenso-template vs inapp),
"Test connection" button
- /admin/branding logo URL, primary color, app name, email
header/footer HTML
- /admin/reminders port-level defaults for new interests +
port-wide daily-digest delivery window
- /admin/invitations send / list / resend / revoke CRM invitations
Per-user reminder digest
- /notifications/preferences gains a Reminder digest card:
immediate / daily / weekly / off, with HH:MM, day-of-week,
IANA timezone fields. Stored in user_profiles.preferences.reminders.
Plumbing
- port-config.ts typed accessors (getPortEmailConfig, getPortDocumensoConfig,
getPortBrandingConfig, getPortReminderConfig) — settings → env fallback.
- sendEmail accepts optional portId; resolves From/SMTP from settings
when supplied.
- documensoFetch + downloadSignedPdf accept optional portId; each public
function takes it through. checkDocumensoHealth() backs the test button.
- crm-invite.service gains listCrmInvites / revokeCrmInvite / resendCrmInvite
with audit-log entries (revoke_invite, resend_invite added to AuditAction).
- AdminLandingPage card grid + shared SettingsFormCard component to remove
per-page form boilerplate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:21:54 +02:00
|
|
|
const cfg = await getPortDocumensoConfig(portId);
|
2026-04-28 02:22:04 +02:00
|
|
|
return { baseUrl: cfg.apiUrl, apiKey: cfg.apiKey, apiVersion: cfg.apiVersion };
|
feat(admin): per-port email/Documenso/branding/reminder settings + invitations
Centralizes everything operators need to configure into the admin panel,
each setting per-port with env fallback.
New admin pages
- /admin landing page linking to every admin section as a card
- /admin/email FROM name+address, reply-to, signature/footer HTML,
optional SMTP host/port/user/pass override
- /admin/documenso API URL+key override, EOI Documenso template ID,
default EOI pathway (documenso-template vs inapp),
"Test connection" button
- /admin/branding logo URL, primary color, app name, email
header/footer HTML
- /admin/reminders port-level defaults for new interests +
port-wide daily-digest delivery window
- /admin/invitations send / list / resend / revoke CRM invitations
Per-user reminder digest
- /notifications/preferences gains a Reminder digest card:
immediate / daily / weekly / off, with HH:MM, day-of-week,
IANA timezone fields. Stored in user_profiles.preferences.reminders.
Plumbing
- port-config.ts typed accessors (getPortEmailConfig, getPortDocumensoConfig,
getPortBrandingConfig, getPortReminderConfig) — settings → env fallback.
- sendEmail accepts optional portId; resolves From/SMTP from settings
when supplied.
- documensoFetch + downloadSignedPdf accept optional portId; each public
function takes it through. checkDocumensoHealth() backs the test button.
- crm-invite.service gains listCrmInvites / revokeCrmInvite / resendCrmInvite
with audit-log entries (revoke_invite, resend_invite added to AuditAction).
- AdminLandingPage card grid + shared SettingsFormCard component to remove
per-page form boilerplate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:21:54 +02:00
|
|
|
}
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
|
feat(admin): per-port email/Documenso/branding/reminder settings + invitations
Centralizes everything operators need to configure into the admin panel,
each setting per-port with env fallback.
New admin pages
- /admin landing page linking to every admin section as a card
- /admin/email FROM name+address, reply-to, signature/footer HTML,
optional SMTP host/port/user/pass override
- /admin/documenso API URL+key override, EOI Documenso template ID,
default EOI pathway (documenso-template vs inapp),
"Test connection" button
- /admin/branding logo URL, primary color, app name, email
header/footer HTML
- /admin/reminders port-level defaults for new interests +
port-wide daily-digest delivery window
- /admin/invitations send / list / resend / revoke CRM invitations
Per-user reminder digest
- /notifications/preferences gains a Reminder digest card:
immediate / daily / weekly / off, with HH:MM, day-of-week,
IANA timezone fields. Stored in user_profiles.preferences.reminders.
Plumbing
- port-config.ts typed accessors (getPortEmailConfig, getPortDocumensoConfig,
getPortBrandingConfig, getPortReminderConfig) — settings → env fallback.
- sendEmail accepts optional portId; resolves From/SMTP from settings
when supplied.
- documensoFetch + downloadSignedPdf accept optional portId; each public
function takes it through. checkDocumensoHealth() backs the test button.
- crm-invite.service gains listCrmInvites / revokeCrmInvite / resendCrmInvite
with audit-log entries (revoke_invite, resend_invite added to AuditAction).
- AdminLandingPage card grid + shared SettingsFormCard component to remove
per-page form boilerplate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:21:54 +02:00
|
|
|
async function documensoFetch(
|
|
|
|
|
path: string,
|
|
|
|
|
options?: RequestInit,
|
|
|
|
|
portId?: string,
|
|
|
|
|
): Promise<unknown> {
|
|
|
|
|
const { baseUrl, apiKey } = await resolveCreds(portId);
|
fix(audit-tier-1): timeouts, lifecycle, per-port Documenso, FK constraints
Closes the second wave of HIGH-priority audit findings:
* fetchWithTimeout helper (new src/lib/fetch-with-timeout.ts) wraps
Documenso, OCR, currency, Umami, IMAP, etc. — a hung upstream can
no longer pin a worker concurrency slot indefinitely. OpenAI client
passes timeout: 30_000. ImapFlow gets socket / greeting / connection
timeouts.
* SIGTERM / SIGINT handler in src/server.ts drains in-flight HTTP,
closes Socket.io, and disconnects Redis before exit; compose
stop_grace_period bumped to 30s. Adds closeSocketServer() helper.
* env.ts gains zod-validated PORT and MULTI_NODE_DEPLOYMENT, and
filesystem.ts now reads from env (a typo can no longer silently
disable the multi-node guard).
* Per-port Documenso template + recipient IDs land in system_settings
with env fallback (PortDocumensoConfig now exposes eoiTemplateId,
clientRecipientId, developerRecipientId, approvalRecipientId).
document-templates.ts uses the per-port config and threads portId
into documensoGenerateFromTemplate().
* Migration 0042 wires the eleven HIGH-tier missing FK constraints
(documents/files/interests/reminders/berth_waiting_list/
form_submissions) plus polymorphic CHECK round 2
(yacht_ownership_history.owner_type, document_sends.document_kind),
invoices.billing_entity_id NOT EMPTY, and clients.merged_into self-FK.
Drizzle schema columns updated to .references(...) where possible
so the misleading "FK wired in relations.ts" comments are gone.
Test status: 1168/1168 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §§5,6,7,8,9,10 +
MED §§14,15,16,18.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:52:58 +02:00
|
|
|
const res = await fetchWithTimeout(`${baseUrl}${path}`, {
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
...options,
|
|
|
|
|
headers: {
|
feat(admin): per-port email/Documenso/branding/reminder settings + invitations
Centralizes everything operators need to configure into the admin panel,
each setting per-port with env fallback.
New admin pages
- /admin landing page linking to every admin section as a card
- /admin/email FROM name+address, reply-to, signature/footer HTML,
optional SMTP host/port/user/pass override
- /admin/documenso API URL+key override, EOI Documenso template ID,
default EOI pathway (documenso-template vs inapp),
"Test connection" button
- /admin/branding logo URL, primary color, app name, email
header/footer HTML
- /admin/reminders port-level defaults for new interests +
port-wide daily-digest delivery window
- /admin/invitations send / list / resend / revoke CRM invitations
Per-user reminder digest
- /notifications/preferences gains a Reminder digest card:
immediate / daily / weekly / off, with HH:MM, day-of-week,
IANA timezone fields. Stored in user_profiles.preferences.reminders.
Plumbing
- port-config.ts typed accessors (getPortEmailConfig, getPortDocumensoConfig,
getPortBrandingConfig, getPortReminderConfig) — settings → env fallback.
- sendEmail accepts optional portId; resolves From/SMTP from settings
when supplied.
- documensoFetch + downloadSignedPdf accept optional portId; each public
function takes it through. checkDocumensoHealth() backs the test button.
- crm-invite.service gains listCrmInvites / revokeCrmInvite / resendCrmInvite
with audit-log entries (revoke_invite, resend_invite added to AuditAction).
- AdminLandingPage card grid + shared SettingsFormCard component to remove
per-page form boilerplate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:21:54 +02:00
|
|
|
Authorization: `Bearer ${apiKey}`,
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
...options?.headers,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const err = await res.text();
|
feat(admin): per-port email/Documenso/branding/reminder settings + invitations
Centralizes everything operators need to configure into the admin panel,
each setting per-port with env fallback.
New admin pages
- /admin landing page linking to every admin section as a card
- /admin/email FROM name+address, reply-to, signature/footer HTML,
optional SMTP host/port/user/pass override
- /admin/documenso API URL+key override, EOI Documenso template ID,
default EOI pathway (documenso-template vs inapp),
"Test connection" button
- /admin/branding logo URL, primary color, app name, email
header/footer HTML
- /admin/reminders port-level defaults for new interests +
port-wide daily-digest delivery window
- /admin/invitations send / list / resend / revoke CRM invitations
Per-user reminder digest
- /notifications/preferences gains a Reminder digest card:
immediate / daily / weekly / off, with HH:MM, day-of-week,
IANA timezone fields. Stored in user_profiles.preferences.reminders.
Plumbing
- port-config.ts typed accessors (getPortEmailConfig, getPortDocumensoConfig,
getPortBrandingConfig, getPortReminderConfig) — settings → env fallback.
- sendEmail accepts optional portId; resolves From/SMTP from settings
when supplied.
- documensoFetch + downloadSignedPdf accept optional portId; each public
function takes it through. checkDocumensoHealth() backs the test button.
- crm-invite.service gains listCrmInvites / revokeCrmInvite / resendCrmInvite
with audit-log entries (revoke_invite, resend_invite added to AuditAction).
- AdminLandingPage card grid + shared SettingsFormCard component to remove
per-page form boilerplate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:21:54 +02:00
|
|
|
logger.error({ path, status: res.status, err, portId }, 'Documenso API error');
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
throw new Error(`Documenso API error: ${res.status}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 15:25:06 +02:00
|
|
|
// Documenso 2.x renamed top-level `id` → `documentId` and recipient `id` →
|
|
|
|
|
// `recipientId`; v1.13 still uses `id`. Normalize both shapes to the legacy
|
|
|
|
|
// `id` form that this codebase consumes everywhere downstream.
|
|
|
|
|
function normalizeDocument(raw: unknown): DocumensoDocument {
|
|
|
|
|
const r = (raw ?? {}) as Record<string, unknown>;
|
|
|
|
|
const id = String(r.documentId ?? r.id ?? '');
|
|
|
|
|
const status = String(r.status ?? 'PENDING');
|
|
|
|
|
const recipientsRaw = (r.recipients as Array<Record<string, unknown>> | undefined) ?? [];
|
|
|
|
|
const recipients = recipientsRaw.map((rec) => ({
|
|
|
|
|
id: String(rec.recipientId ?? rec.id ?? ''),
|
|
|
|
|
name: String(rec.name ?? ''),
|
|
|
|
|
email: String(rec.email ?? ''),
|
|
|
|
|
role: String(rec.role ?? ''),
|
|
|
|
|
signingOrder: Number(rec.signingOrder ?? 0),
|
|
|
|
|
status: String(rec.signingStatus ?? rec.status ?? 'PENDING'),
|
|
|
|
|
signingUrl: typeof rec.signingUrl === 'string' ? rec.signingUrl : undefined,
|
|
|
|
|
embeddedUrl: typeof rec.embeddedUrl === 'string' ? rec.embeddedUrl : undefined,
|
|
|
|
|
}));
|
|
|
|
|
return { id, status, recipients };
|
|
|
|
|
}
|
|
|
|
|
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
export interface DocumensoRecipient {
|
|
|
|
|
name: string;
|
|
|
|
|
email: string;
|
|
|
|
|
role: string;
|
|
|
|
|
signingOrder: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface DocumensoDocument {
|
|
|
|
|
id: string;
|
|
|
|
|
status: string;
|
|
|
|
|
recipients: Array<{
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
email: string;
|
|
|
|
|
role: string;
|
|
|
|
|
signingOrder: number;
|
|
|
|
|
status: string;
|
|
|
|
|
signingUrl?: string;
|
|
|
|
|
embeddedUrl?: string;
|
|
|
|
|
}>;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 17:24:41 +02:00
|
|
|
/**
|
|
|
|
|
* When EMAIL_REDIRECT_TO is set (dev / staging), rewrite every recipient
|
|
|
|
|
* email so Documenso doesn't accidentally email real clients during a
|
|
|
|
|
* data import / migration dry-run. Names are prefixed with the original
|
|
|
|
|
* email so the recipient (you) can tell who would have received the doc.
|
|
|
|
|
*
|
|
|
|
|
* In production this env var is unset and recipients flow through unchanged.
|
|
|
|
|
*/
|
|
|
|
|
function applyRecipientRedirect(recipients: DocumensoRecipient[]): DocumensoRecipient[] {
|
|
|
|
|
if (!env.EMAIL_REDIRECT_TO) return recipients;
|
|
|
|
|
return recipients.map((r) => ({
|
|
|
|
|
...r,
|
|
|
|
|
name: `${r.name} (was: ${r.email})`,
|
|
|
|
|
email: env.EMAIL_REDIRECT_TO!,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Same idea for the template-generate endpoint, which takes a payload
|
|
|
|
|
* shape with recipient email/name nested inside `formValues` (Documenso
|
|
|
|
|
* v1.13) or `recipients` (Documenso 2.x). We rewrite both shapes.
|
|
|
|
|
*/
|
|
|
|
|
function applyPayloadRedirect(payload: Record<string, unknown>): Record<string, unknown> {
|
|
|
|
|
if (!env.EMAIL_REDIRECT_TO) return payload;
|
|
|
|
|
const out: Record<string, unknown> = { ...payload };
|
|
|
|
|
// 2.x recipient shape
|
|
|
|
|
if (Array.isArray(out.recipients)) {
|
|
|
|
|
out.recipients = (out.recipients as Array<Record<string, unknown>>).map((r) => ({
|
|
|
|
|
...r,
|
|
|
|
|
name: `${String(r.name ?? '')} (was: ${String(r.email ?? '')})`,
|
|
|
|
|
email: env.EMAIL_REDIRECT_TO,
|
|
|
|
|
}));
|
|
|
|
|
}
|
2026-05-04 22:57:01 +02:00
|
|
|
// v1.13 formValues shape - keys vary per template; key by anything that
|
2026-05-03 17:24:41 +02:00
|
|
|
// looks like an email field. The conservative approach: only touch keys
|
|
|
|
|
// that already hold a string and end with `Email` / `email`.
|
|
|
|
|
if (out.formValues && typeof out.formValues === 'object') {
|
|
|
|
|
const fv = { ...(out.formValues as Record<string, unknown>) };
|
|
|
|
|
for (const key of Object.keys(fv)) {
|
|
|
|
|
if (/email$/i.test(key) && typeof fv[key] === 'string') {
|
|
|
|
|
fv[key] = env.EMAIL_REDIRECT_TO;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
out.formValues = fv;
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
export async function createDocument(
|
|
|
|
|
title: string,
|
|
|
|
|
pdfBase64: string,
|
|
|
|
|
recipients: DocumensoRecipient[],
|
feat(admin): per-port email/Documenso/branding/reminder settings + invitations
Centralizes everything operators need to configure into the admin panel,
each setting per-port with env fallback.
New admin pages
- /admin landing page linking to every admin section as a card
- /admin/email FROM name+address, reply-to, signature/footer HTML,
optional SMTP host/port/user/pass override
- /admin/documenso API URL+key override, EOI Documenso template ID,
default EOI pathway (documenso-template vs inapp),
"Test connection" button
- /admin/branding logo URL, primary color, app name, email
header/footer HTML
- /admin/reminders port-level defaults for new interests +
port-wide daily-digest delivery window
- /admin/invitations send / list / resend / revoke CRM invitations
Per-user reminder digest
- /notifications/preferences gains a Reminder digest card:
immediate / daily / weekly / off, with HH:MM, day-of-week,
IANA timezone fields. Stored in user_profiles.preferences.reminders.
Plumbing
- port-config.ts typed accessors (getPortEmailConfig, getPortDocumensoConfig,
getPortBrandingConfig, getPortReminderConfig) — settings → env fallback.
- sendEmail accepts optional portId; resolves From/SMTP from settings
when supplied.
- documensoFetch + downloadSignedPdf accept optional portId; each public
function takes it through. checkDocumensoHealth() backs the test button.
- crm-invite.service gains listCrmInvites / revokeCrmInvite / resendCrmInvite
with audit-log entries (revoke_invite, resend_invite added to AuditAction).
- AdminLandingPage card grid + shared SettingsFormCard component to remove
per-page form boilerplate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:21:54 +02:00
|
|
|
portId?: string,
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
): Promise<DocumensoDocument> {
|
2026-05-03 17:24:41 +02:00
|
|
|
const safeRecipients = applyRecipientRedirect(recipients);
|
|
|
|
|
if (env.EMAIL_REDIRECT_TO) {
|
|
|
|
|
logger.info(
|
|
|
|
|
{ redirected: safeRecipients.length, original: recipients.map((r) => r.email) },
|
|
|
|
|
'Documenso recipients redirected to EMAIL_REDIRECT_TO',
|
|
|
|
|
);
|
|
|
|
|
}
|
feat(admin): per-port email/Documenso/branding/reminder settings + invitations
Centralizes everything operators need to configure into the admin panel,
each setting per-port with env fallback.
New admin pages
- /admin landing page linking to every admin section as a card
- /admin/email FROM name+address, reply-to, signature/footer HTML,
optional SMTP host/port/user/pass override
- /admin/documenso API URL+key override, EOI Documenso template ID,
default EOI pathway (documenso-template vs inapp),
"Test connection" button
- /admin/branding logo URL, primary color, app name, email
header/footer HTML
- /admin/reminders port-level defaults for new interests +
port-wide daily-digest delivery window
- /admin/invitations send / list / resend / revoke CRM invitations
Per-user reminder digest
- /notifications/preferences gains a Reminder digest card:
immediate / daily / weekly / off, with HH:MM, day-of-week,
IANA timezone fields. Stored in user_profiles.preferences.reminders.
Plumbing
- port-config.ts typed accessors (getPortEmailConfig, getPortDocumensoConfig,
getPortBrandingConfig, getPortReminderConfig) — settings → env fallback.
- sendEmail accepts optional portId; resolves From/SMTP from settings
when supplied.
- documensoFetch + downloadSignedPdf accept optional portId; each public
function takes it through. checkDocumensoHealth() backs the test button.
- crm-invite.service gains listCrmInvites / revokeCrmInvite / resendCrmInvite
with audit-log entries (revoke_invite, resend_invite added to AuditAction).
- AdminLandingPage card grid + shared SettingsFormCard component to remove
per-page form boilerplate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:21:54 +02:00
|
|
|
return documensoFetch(
|
|
|
|
|
'/api/v1/documents',
|
|
|
|
|
{
|
|
|
|
|
method: 'POST',
|
2026-05-03 17:24:41 +02:00
|
|
|
body: JSON.stringify({ title, document: pdfBase64, recipients: safeRecipients }),
|
feat(admin): per-port email/Documenso/branding/reminder settings + invitations
Centralizes everything operators need to configure into the admin panel,
each setting per-port with env fallback.
New admin pages
- /admin landing page linking to every admin section as a card
- /admin/email FROM name+address, reply-to, signature/footer HTML,
optional SMTP host/port/user/pass override
- /admin/documenso API URL+key override, EOI Documenso template ID,
default EOI pathway (documenso-template vs inapp),
"Test connection" button
- /admin/branding logo URL, primary color, app name, email
header/footer HTML
- /admin/reminders port-level defaults for new interests +
port-wide daily-digest delivery window
- /admin/invitations send / list / resend / revoke CRM invitations
Per-user reminder digest
- /notifications/preferences gains a Reminder digest card:
immediate / daily / weekly / off, with HH:MM, day-of-week,
IANA timezone fields. Stored in user_profiles.preferences.reminders.
Plumbing
- port-config.ts typed accessors (getPortEmailConfig, getPortDocumensoConfig,
getPortBrandingConfig, getPortReminderConfig) — settings → env fallback.
- sendEmail accepts optional portId; resolves From/SMTP from settings
when supplied.
- documensoFetch + downloadSignedPdf accept optional portId; each public
function takes it through. checkDocumensoHealth() backs the test button.
- crm-invite.service gains listCrmInvites / revokeCrmInvite / resendCrmInvite
with audit-log entries (revoke_invite, resend_invite added to AuditAction).
- AdminLandingPage card grid + shared SettingsFormCard component to remove
per-page form boilerplate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:21:54 +02:00
|
|
|
},
|
|
|
|
|
portId,
|
|
|
|
|
).then(normalizeDocument);
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-24 18:43:41 +02:00
|
|
|
export async function generateDocumentFromTemplate(
|
|
|
|
|
templateId: number,
|
|
|
|
|
payload: Record<string, unknown>,
|
feat(admin): per-port email/Documenso/branding/reminder settings + invitations
Centralizes everything operators need to configure into the admin panel,
each setting per-port with env fallback.
New admin pages
- /admin landing page linking to every admin section as a card
- /admin/email FROM name+address, reply-to, signature/footer HTML,
optional SMTP host/port/user/pass override
- /admin/documenso API URL+key override, EOI Documenso template ID,
default EOI pathway (documenso-template vs inapp),
"Test connection" button
- /admin/branding logo URL, primary color, app name, email
header/footer HTML
- /admin/reminders port-level defaults for new interests +
port-wide daily-digest delivery window
- /admin/invitations send / list / resend / revoke CRM invitations
Per-user reminder digest
- /notifications/preferences gains a Reminder digest card:
immediate / daily / weekly / off, with HH:MM, day-of-week,
IANA timezone fields. Stored in user_profiles.preferences.reminders.
Plumbing
- port-config.ts typed accessors (getPortEmailConfig, getPortDocumensoConfig,
getPortBrandingConfig, getPortReminderConfig) — settings → env fallback.
- sendEmail accepts optional portId; resolves From/SMTP from settings
when supplied.
- documensoFetch + downloadSignedPdf accept optional portId; each public
function takes it through. checkDocumensoHealth() backs the test button.
- crm-invite.service gains listCrmInvites / revokeCrmInvite / resendCrmInvite
with audit-log entries (revoke_invite, resend_invite added to AuditAction).
- AdminLandingPage card grid + shared SettingsFormCard component to remove
per-page form boilerplate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:21:54 +02:00
|
|
|
portId?: string,
|
2026-04-24 18:43:41 +02:00
|
|
|
): Promise<DocumensoDocument> {
|
2026-05-03 17:24:41 +02:00
|
|
|
const safePayload = applyPayloadRedirect(payload);
|
|
|
|
|
if (env.EMAIL_REDIRECT_TO) {
|
|
|
|
|
logger.info(
|
|
|
|
|
{ templateId },
|
|
|
|
|
'Documenso template-generate payload redirected to EMAIL_REDIRECT_TO',
|
|
|
|
|
);
|
|
|
|
|
}
|
feat(admin): per-port email/Documenso/branding/reminder settings + invitations
Centralizes everything operators need to configure into the admin panel,
each setting per-port with env fallback.
New admin pages
- /admin landing page linking to every admin section as a card
- /admin/email FROM name+address, reply-to, signature/footer HTML,
optional SMTP host/port/user/pass override
- /admin/documenso API URL+key override, EOI Documenso template ID,
default EOI pathway (documenso-template vs inapp),
"Test connection" button
- /admin/branding logo URL, primary color, app name, email
header/footer HTML
- /admin/reminders port-level defaults for new interests +
port-wide daily-digest delivery window
- /admin/invitations send / list / resend / revoke CRM invitations
Per-user reminder digest
- /notifications/preferences gains a Reminder digest card:
immediate / daily / weekly / off, with HH:MM, day-of-week,
IANA timezone fields. Stored in user_profiles.preferences.reminders.
Plumbing
- port-config.ts typed accessors (getPortEmailConfig, getPortDocumensoConfig,
getPortBrandingConfig, getPortReminderConfig) — settings → env fallback.
- sendEmail accepts optional portId; resolves From/SMTP from settings
when supplied.
- documensoFetch + downloadSignedPdf accept optional portId; each public
function takes it through. checkDocumensoHealth() backs the test button.
- crm-invite.service gains listCrmInvites / revokeCrmInvite / resendCrmInvite
with audit-log entries (revoke_invite, resend_invite added to AuditAction).
- AdminLandingPage card grid + shared SettingsFormCard component to remove
per-page form boilerplate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:21:54 +02:00
|
|
|
return documensoFetch(
|
|
|
|
|
`/api/v1/templates/${templateId}/generate-document`,
|
|
|
|
|
{
|
|
|
|
|
method: 'POST',
|
2026-05-03 17:24:41 +02:00
|
|
|
body: JSON.stringify(safePayload),
|
feat(admin): per-port email/Documenso/branding/reminder settings + invitations
Centralizes everything operators need to configure into the admin panel,
each setting per-port with env fallback.
New admin pages
- /admin landing page linking to every admin section as a card
- /admin/email FROM name+address, reply-to, signature/footer HTML,
optional SMTP host/port/user/pass override
- /admin/documenso API URL+key override, EOI Documenso template ID,
default EOI pathway (documenso-template vs inapp),
"Test connection" button
- /admin/branding logo URL, primary color, app name, email
header/footer HTML
- /admin/reminders port-level defaults for new interests +
port-wide daily-digest delivery window
- /admin/invitations send / list / resend / revoke CRM invitations
Per-user reminder digest
- /notifications/preferences gains a Reminder digest card:
immediate / daily / weekly / off, with HH:MM, day-of-week,
IANA timezone fields. Stored in user_profiles.preferences.reminders.
Plumbing
- port-config.ts typed accessors (getPortEmailConfig, getPortDocumensoConfig,
getPortBrandingConfig, getPortReminderConfig) — settings → env fallback.
- sendEmail accepts optional portId; resolves From/SMTP from settings
when supplied.
- documensoFetch + downloadSignedPdf accept optional portId; each public
function takes it through. checkDocumensoHealth() backs the test button.
- crm-invite.service gains listCrmInvites / revokeCrmInvite / resendCrmInvite
with audit-log entries (revoke_invite, resend_invite added to AuditAction).
- AdminLandingPage card grid + shared SettingsFormCard component to remove
per-page form boilerplate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:21:54 +02:00
|
|
|
},
|
|
|
|
|
portId,
|
|
|
|
|
).then(normalizeDocument);
|
2026-04-24 18:43:41 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-03 20:55:53 +02:00
|
|
|
/**
|
|
|
|
|
* Tell Documenso to actually email the document to its recipients. The
|
|
|
|
|
* recipients themselves are set at create-time (and rerouted to
|
|
|
|
|
* EMAIL_REDIRECT_TO when set), but this is a belt-and-braces guard for
|
|
|
|
|
* documents that may have been created BEFORE the redirect was turned on
|
|
|
|
|
* (i.e. real-recipient documents now triggered by an automation while
|
|
|
|
|
* we're trying to hold comms). When the redirect is on we skip the API
|
|
|
|
|
* call entirely and return a synthetic "still pending" response.
|
|
|
|
|
*/
|
feat(admin): per-port email/Documenso/branding/reminder settings + invitations
Centralizes everything operators need to configure into the admin panel,
each setting per-port with env fallback.
New admin pages
- /admin landing page linking to every admin section as a card
- /admin/email FROM name+address, reply-to, signature/footer HTML,
optional SMTP host/port/user/pass override
- /admin/documenso API URL+key override, EOI Documenso template ID,
default EOI pathway (documenso-template vs inapp),
"Test connection" button
- /admin/branding logo URL, primary color, app name, email
header/footer HTML
- /admin/reminders port-level defaults for new interests +
port-wide daily-digest delivery window
- /admin/invitations send / list / resend / revoke CRM invitations
Per-user reminder digest
- /notifications/preferences gains a Reminder digest card:
immediate / daily / weekly / off, with HH:MM, day-of-week,
IANA timezone fields. Stored in user_profiles.preferences.reminders.
Plumbing
- port-config.ts typed accessors (getPortEmailConfig, getPortDocumensoConfig,
getPortBrandingConfig, getPortReminderConfig) — settings → env fallback.
- sendEmail accepts optional portId; resolves From/SMTP from settings
when supplied.
- documensoFetch + downloadSignedPdf accept optional portId; each public
function takes it through. checkDocumensoHealth() backs the test button.
- crm-invite.service gains listCrmInvites / revokeCrmInvite / resendCrmInvite
with audit-log entries (revoke_invite, resend_invite added to AuditAction).
- AdminLandingPage card grid + shared SettingsFormCard component to remove
per-page form boilerplate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:21:54 +02:00
|
|
|
export async function sendDocument(docId: string, portId?: string): Promise<DocumensoDocument> {
|
2026-05-03 20:55:53 +02:00
|
|
|
if (env.EMAIL_REDIRECT_TO) {
|
|
|
|
|
logger.warn(
|
|
|
|
|
{ docId, portId, redirect: env.EMAIL_REDIRECT_TO },
|
2026-05-04 22:57:01 +02:00
|
|
|
'sendDocument SKIPPED - EMAIL_REDIRECT_TO is set, outbound comms paused',
|
2026-05-03 20:55:53 +02:00
|
|
|
);
|
|
|
|
|
// Return the existing doc shape so downstream code doesn't see an
|
|
|
|
|
// unexpected null. The document remains in DRAFT/PENDING from
|
|
|
|
|
// Documenso's perspective.
|
|
|
|
|
return getDocument(docId, portId);
|
|
|
|
|
}
|
feat(admin): per-port email/Documenso/branding/reminder settings + invitations
Centralizes everything operators need to configure into the admin panel,
each setting per-port with env fallback.
New admin pages
- /admin landing page linking to every admin section as a card
- /admin/email FROM name+address, reply-to, signature/footer HTML,
optional SMTP host/port/user/pass override
- /admin/documenso API URL+key override, EOI Documenso template ID,
default EOI pathway (documenso-template vs inapp),
"Test connection" button
- /admin/branding logo URL, primary color, app name, email
header/footer HTML
- /admin/reminders port-level defaults for new interests +
port-wide daily-digest delivery window
- /admin/invitations send / list / resend / revoke CRM invitations
Per-user reminder digest
- /notifications/preferences gains a Reminder digest card:
immediate / daily / weekly / off, with HH:MM, day-of-week,
IANA timezone fields. Stored in user_profiles.preferences.reminders.
Plumbing
- port-config.ts typed accessors (getPortEmailConfig, getPortDocumensoConfig,
getPortBrandingConfig, getPortReminderConfig) — settings → env fallback.
- sendEmail accepts optional portId; resolves From/SMTP from settings
when supplied.
- documensoFetch + downloadSignedPdf accept optional portId; each public
function takes it through. checkDocumensoHealth() backs the test button.
- crm-invite.service gains listCrmInvites / revokeCrmInvite / resendCrmInvite
with audit-log entries (revoke_invite, resend_invite added to AuditAction).
- AdminLandingPage card grid + shared SettingsFormCard component to remove
per-page form boilerplate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:21:54 +02:00
|
|
|
return documensoFetch(
|
|
|
|
|
`/api/v1/documents/${docId}/send`,
|
|
|
|
|
{
|
|
|
|
|
method: 'POST',
|
|
|
|
|
},
|
|
|
|
|
portId,
|
|
|
|
|
).then(normalizeDocument);
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
}
|
|
|
|
|
|
feat(admin): per-port email/Documenso/branding/reminder settings + invitations
Centralizes everything operators need to configure into the admin panel,
each setting per-port with env fallback.
New admin pages
- /admin landing page linking to every admin section as a card
- /admin/email FROM name+address, reply-to, signature/footer HTML,
optional SMTP host/port/user/pass override
- /admin/documenso API URL+key override, EOI Documenso template ID,
default EOI pathway (documenso-template vs inapp),
"Test connection" button
- /admin/branding logo URL, primary color, app name, email
header/footer HTML
- /admin/reminders port-level defaults for new interests +
port-wide daily-digest delivery window
- /admin/invitations send / list / resend / revoke CRM invitations
Per-user reminder digest
- /notifications/preferences gains a Reminder digest card:
immediate / daily / weekly / off, with HH:MM, day-of-week,
IANA timezone fields. Stored in user_profiles.preferences.reminders.
Plumbing
- port-config.ts typed accessors (getPortEmailConfig, getPortDocumensoConfig,
getPortBrandingConfig, getPortReminderConfig) — settings → env fallback.
- sendEmail accepts optional portId; resolves From/SMTP from settings
when supplied.
- documensoFetch + downloadSignedPdf accept optional portId; each public
function takes it through. checkDocumensoHealth() backs the test button.
- crm-invite.service gains listCrmInvites / revokeCrmInvite / resendCrmInvite
with audit-log entries (revoke_invite, resend_invite added to AuditAction).
- AdminLandingPage card grid + shared SettingsFormCard component to remove
per-page form boilerplate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:21:54 +02:00
|
|
|
export async function getDocument(docId: string, portId?: string): Promise<DocumensoDocument> {
|
|
|
|
|
return documensoFetch(`/api/v1/documents/${docId}`, undefined, portId).then(normalizeDocument);
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
}
|
|
|
|
|
|
2026-05-03 20:55:53 +02:00
|
|
|
/**
|
|
|
|
|
* Email a signing reminder to one recipient. Skipped entirely when
|
2026-05-04 22:57:01 +02:00
|
|
|
* EMAIL_REDIRECT_TO is set - the recipient's stored email may still be
|
2026-05-03 20:55:53 +02:00
|
|
|
* a real client address from before the redirect was enabled.
|
|
|
|
|
*/
|
feat(admin): per-port email/Documenso/branding/reminder settings + invitations
Centralizes everything operators need to configure into the admin panel,
each setting per-port with env fallback.
New admin pages
- /admin landing page linking to every admin section as a card
- /admin/email FROM name+address, reply-to, signature/footer HTML,
optional SMTP host/port/user/pass override
- /admin/documenso API URL+key override, EOI Documenso template ID,
default EOI pathway (documenso-template vs inapp),
"Test connection" button
- /admin/branding logo URL, primary color, app name, email
header/footer HTML
- /admin/reminders port-level defaults for new interests +
port-wide daily-digest delivery window
- /admin/invitations send / list / resend / revoke CRM invitations
Per-user reminder digest
- /notifications/preferences gains a Reminder digest card:
immediate / daily / weekly / off, with HH:MM, day-of-week,
IANA timezone fields. Stored in user_profiles.preferences.reminders.
Plumbing
- port-config.ts typed accessors (getPortEmailConfig, getPortDocumensoConfig,
getPortBrandingConfig, getPortReminderConfig) — settings → env fallback.
- sendEmail accepts optional portId; resolves From/SMTP from settings
when supplied.
- documensoFetch + downloadSignedPdf accept optional portId; each public
function takes it through. checkDocumensoHealth() backs the test button.
- crm-invite.service gains listCrmInvites / revokeCrmInvite / resendCrmInvite
with audit-log entries (revoke_invite, resend_invite added to AuditAction).
- AdminLandingPage card grid + shared SettingsFormCard component to remove
per-page form boilerplate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:21:54 +02:00
|
|
|
export async function sendReminder(
|
|
|
|
|
docId: string,
|
|
|
|
|
signerId: string,
|
|
|
|
|
portId?: string,
|
|
|
|
|
): Promise<void> {
|
2026-05-03 20:55:53 +02:00
|
|
|
if (env.EMAIL_REDIRECT_TO) {
|
|
|
|
|
logger.warn(
|
|
|
|
|
{ docId, signerId, portId, redirect: env.EMAIL_REDIRECT_TO },
|
2026-05-04 22:57:01 +02:00
|
|
|
'sendReminder SKIPPED - EMAIL_REDIRECT_TO is set, outbound comms paused',
|
2026-05-03 20:55:53 +02:00
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
feat(admin): per-port email/Documenso/branding/reminder settings + invitations
Centralizes everything operators need to configure into the admin panel,
each setting per-port with env fallback.
New admin pages
- /admin landing page linking to every admin section as a card
- /admin/email FROM name+address, reply-to, signature/footer HTML,
optional SMTP host/port/user/pass override
- /admin/documenso API URL+key override, EOI Documenso template ID,
default EOI pathway (documenso-template vs inapp),
"Test connection" button
- /admin/branding logo URL, primary color, app name, email
header/footer HTML
- /admin/reminders port-level defaults for new interests +
port-wide daily-digest delivery window
- /admin/invitations send / list / resend / revoke CRM invitations
Per-user reminder digest
- /notifications/preferences gains a Reminder digest card:
immediate / daily / weekly / off, with HH:MM, day-of-week,
IANA timezone fields. Stored in user_profiles.preferences.reminders.
Plumbing
- port-config.ts typed accessors (getPortEmailConfig, getPortDocumensoConfig,
getPortBrandingConfig, getPortReminderConfig) — settings → env fallback.
- sendEmail accepts optional portId; resolves From/SMTP from settings
when supplied.
- documensoFetch + downloadSignedPdf accept optional portId; each public
function takes it through. checkDocumensoHealth() backs the test button.
- crm-invite.service gains listCrmInvites / revokeCrmInvite / resendCrmInvite
with audit-log entries (revoke_invite, resend_invite added to AuditAction).
- AdminLandingPage card grid + shared SettingsFormCard component to remove
per-page form boilerplate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:21:54 +02:00
|
|
|
await documensoFetch(
|
|
|
|
|
`/api/v1/documents/${docId}/recipients/${signerId}/remind`,
|
|
|
|
|
{
|
|
|
|
|
method: 'POST',
|
|
|
|
|
},
|
|
|
|
|
portId,
|
|
|
|
|
);
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
}
|
|
|
|
|
|
feat(admin): per-port email/Documenso/branding/reminder settings + invitations
Centralizes everything operators need to configure into the admin panel,
each setting per-port with env fallback.
New admin pages
- /admin landing page linking to every admin section as a card
- /admin/email FROM name+address, reply-to, signature/footer HTML,
optional SMTP host/port/user/pass override
- /admin/documenso API URL+key override, EOI Documenso template ID,
default EOI pathway (documenso-template vs inapp),
"Test connection" button
- /admin/branding logo URL, primary color, app name, email
header/footer HTML
- /admin/reminders port-level defaults for new interests +
port-wide daily-digest delivery window
- /admin/invitations send / list / resend / revoke CRM invitations
Per-user reminder digest
- /notifications/preferences gains a Reminder digest card:
immediate / daily / weekly / off, with HH:MM, day-of-week,
IANA timezone fields. Stored in user_profiles.preferences.reminders.
Plumbing
- port-config.ts typed accessors (getPortEmailConfig, getPortDocumensoConfig,
getPortBrandingConfig, getPortReminderConfig) — settings → env fallback.
- sendEmail accepts optional portId; resolves From/SMTP from settings
when supplied.
- documensoFetch + downloadSignedPdf accept optional portId; each public
function takes it through. checkDocumensoHealth() backs the test button.
- crm-invite.service gains listCrmInvites / revokeCrmInvite / resendCrmInvite
with audit-log entries (revoke_invite, resend_invite added to AuditAction).
- AdminLandingPage card grid + shared SettingsFormCard component to remove
per-page form boilerplate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:21:54 +02:00
|
|
|
export async function downloadSignedPdf(docId: string, portId?: string): Promise<Buffer> {
|
|
|
|
|
const { baseUrl, apiKey } = await resolveCreds(portId);
|
fix(audit-tier-1): timeouts, lifecycle, per-port Documenso, FK constraints
Closes the second wave of HIGH-priority audit findings:
* fetchWithTimeout helper (new src/lib/fetch-with-timeout.ts) wraps
Documenso, OCR, currency, Umami, IMAP, etc. — a hung upstream can
no longer pin a worker concurrency slot indefinitely. OpenAI client
passes timeout: 30_000. ImapFlow gets socket / greeting / connection
timeouts.
* SIGTERM / SIGINT handler in src/server.ts drains in-flight HTTP,
closes Socket.io, and disconnects Redis before exit; compose
stop_grace_period bumped to 30s. Adds closeSocketServer() helper.
* env.ts gains zod-validated PORT and MULTI_NODE_DEPLOYMENT, and
filesystem.ts now reads from env (a typo can no longer silently
disable the multi-node guard).
* Per-port Documenso template + recipient IDs land in system_settings
with env fallback (PortDocumensoConfig now exposes eoiTemplateId,
clientRecipientId, developerRecipientId, approvalRecipientId).
document-templates.ts uses the per-port config and threads portId
into documensoGenerateFromTemplate().
* Migration 0042 wires the eleven HIGH-tier missing FK constraints
(documents/files/interests/reminders/berth_waiting_list/
form_submissions) plus polymorphic CHECK round 2
(yacht_ownership_history.owner_type, document_sends.document_kind),
invoices.billing_entity_id NOT EMPTY, and clients.merged_into self-FK.
Drizzle schema columns updated to .references(...) where possible
so the misleading "FK wired in relations.ts" comments are gone.
Test status: 1168/1168 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §§5,6,7,8,9,10 +
MED §§14,15,16,18.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:52:58 +02:00
|
|
|
const res = await fetchWithTimeout(`${baseUrl}/api/v1/documents/${docId}/download`, {
|
feat(admin): per-port email/Documenso/branding/reminder settings + invitations
Centralizes everything operators need to configure into the admin panel,
each setting per-port with env fallback.
New admin pages
- /admin landing page linking to every admin section as a card
- /admin/email FROM name+address, reply-to, signature/footer HTML,
optional SMTP host/port/user/pass override
- /admin/documenso API URL+key override, EOI Documenso template ID,
default EOI pathway (documenso-template vs inapp),
"Test connection" button
- /admin/branding logo URL, primary color, app name, email
header/footer HTML
- /admin/reminders port-level defaults for new interests +
port-wide daily-digest delivery window
- /admin/invitations send / list / resend / revoke CRM invitations
Per-user reminder digest
- /notifications/preferences gains a Reminder digest card:
immediate / daily / weekly / off, with HH:MM, day-of-week,
IANA timezone fields. Stored in user_profiles.preferences.reminders.
Plumbing
- port-config.ts typed accessors (getPortEmailConfig, getPortDocumensoConfig,
getPortBrandingConfig, getPortReminderConfig) — settings → env fallback.
- sendEmail accepts optional portId; resolves From/SMTP from settings
when supplied.
- documensoFetch + downloadSignedPdf accept optional portId; each public
function takes it through. checkDocumensoHealth() backs the test button.
- crm-invite.service gains listCrmInvites / revokeCrmInvite / resendCrmInvite
with audit-log entries (revoke_invite, resend_invite added to AuditAction).
- AdminLandingPage card grid + shared SettingsFormCard component to remove
per-page form boilerplate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:21:54 +02:00
|
|
|
headers: { Authorization: `Bearer ${apiKey}` },
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const err = await res.text();
|
feat(admin): per-port email/Documenso/branding/reminder settings + invitations
Centralizes everything operators need to configure into the admin panel,
each setting per-port with env fallback.
New admin pages
- /admin landing page linking to every admin section as a card
- /admin/email FROM name+address, reply-to, signature/footer HTML,
optional SMTP host/port/user/pass override
- /admin/documenso API URL+key override, EOI Documenso template ID,
default EOI pathway (documenso-template vs inapp),
"Test connection" button
- /admin/branding logo URL, primary color, app name, email
header/footer HTML
- /admin/reminders port-level defaults for new interests +
port-wide daily-digest delivery window
- /admin/invitations send / list / resend / revoke CRM invitations
Per-user reminder digest
- /notifications/preferences gains a Reminder digest card:
immediate / daily / weekly / off, with HH:MM, day-of-week,
IANA timezone fields. Stored in user_profiles.preferences.reminders.
Plumbing
- port-config.ts typed accessors (getPortEmailConfig, getPortDocumensoConfig,
getPortBrandingConfig, getPortReminderConfig) — settings → env fallback.
- sendEmail accepts optional portId; resolves From/SMTP from settings
when supplied.
- documensoFetch + downloadSignedPdf accept optional portId; each public
function takes it through. checkDocumensoHealth() backs the test button.
- crm-invite.service gains listCrmInvites / revokeCrmInvite / resendCrmInvite
with audit-log entries (revoke_invite, resend_invite added to AuditAction).
- AdminLandingPage card grid + shared SettingsFormCard component to remove
per-page form boilerplate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:21:54 +02:00
|
|
|
logger.error({ docId, status: res.status, err, portId }, 'Documenso download error');
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
throw new Error(`Documenso download error: ${res.status}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const arrayBuffer = await res.arrayBuffer();
|
|
|
|
|
return Buffer.from(arrayBuffer);
|
|
|
|
|
}
|
feat(admin): per-port email/Documenso/branding/reminder settings + invitations
Centralizes everything operators need to configure into the admin panel,
each setting per-port with env fallback.
New admin pages
- /admin landing page linking to every admin section as a card
- /admin/email FROM name+address, reply-to, signature/footer HTML,
optional SMTP host/port/user/pass override
- /admin/documenso API URL+key override, EOI Documenso template ID,
default EOI pathway (documenso-template vs inapp),
"Test connection" button
- /admin/branding logo URL, primary color, app name, email
header/footer HTML
- /admin/reminders port-level defaults for new interests +
port-wide daily-digest delivery window
- /admin/invitations send / list / resend / revoke CRM invitations
Per-user reminder digest
- /notifications/preferences gains a Reminder digest card:
immediate / daily / weekly / off, with HH:MM, day-of-week,
IANA timezone fields. Stored in user_profiles.preferences.reminders.
Plumbing
- port-config.ts typed accessors (getPortEmailConfig, getPortDocumensoConfig,
getPortBrandingConfig, getPortReminderConfig) — settings → env fallback.
- sendEmail accepts optional portId; resolves From/SMTP from settings
when supplied.
- documensoFetch + downloadSignedPdf accept optional portId; each public
function takes it through. checkDocumensoHealth() backs the test button.
- crm-invite.service gains listCrmInvites / revokeCrmInvite / resendCrmInvite
with audit-log entries (revoke_invite, resend_invite added to AuditAction).
- AdminLandingPage card grid + shared SettingsFormCard component to remove
per-page form boilerplate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:21:54 +02:00
|
|
|
|
|
|
|
|
/** Convenience health-check used by the admin "Test connection" button. */
|
|
|
|
|
export async function checkDocumensoHealth(
|
|
|
|
|
portId?: string,
|
|
|
|
|
): Promise<{ ok: boolean; status?: number; error?: string }> {
|
|
|
|
|
try {
|
|
|
|
|
const { baseUrl, apiKey } = await resolveCreds(portId);
|
fix(audit-tier-1): timeouts, lifecycle, per-port Documenso, FK constraints
Closes the second wave of HIGH-priority audit findings:
* fetchWithTimeout helper (new src/lib/fetch-with-timeout.ts) wraps
Documenso, OCR, currency, Umami, IMAP, etc. — a hung upstream can
no longer pin a worker concurrency slot indefinitely. OpenAI client
passes timeout: 30_000. ImapFlow gets socket / greeting / connection
timeouts.
* SIGTERM / SIGINT handler in src/server.ts drains in-flight HTTP,
closes Socket.io, and disconnects Redis before exit; compose
stop_grace_period bumped to 30s. Adds closeSocketServer() helper.
* env.ts gains zod-validated PORT and MULTI_NODE_DEPLOYMENT, and
filesystem.ts now reads from env (a typo can no longer silently
disable the multi-node guard).
* Per-port Documenso template + recipient IDs land in system_settings
with env fallback (PortDocumensoConfig now exposes eoiTemplateId,
clientRecipientId, developerRecipientId, approvalRecipientId).
document-templates.ts uses the per-port config and threads portId
into documensoGenerateFromTemplate().
* Migration 0042 wires the eleven HIGH-tier missing FK constraints
(documents/files/interests/reminders/berth_waiting_list/
form_submissions) plus polymorphic CHECK round 2
(yacht_ownership_history.owner_type, document_sends.document_kind),
invoices.billing_entity_id NOT EMPTY, and clients.merged_into self-FK.
Drizzle schema columns updated to .references(...) where possible
so the misleading "FK wired in relations.ts" comments are gone.
Test status: 1168/1168 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §§5,6,7,8,9,10 +
MED §§14,15,16,18.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:52:58 +02:00
|
|
|
const res = await fetchWithTimeout(`${baseUrl}/api/v1/health`, {
|
feat(admin): per-port email/Documenso/branding/reminder settings + invitations
Centralizes everything operators need to configure into the admin panel,
each setting per-port with env fallback.
New admin pages
- /admin landing page linking to every admin section as a card
- /admin/email FROM name+address, reply-to, signature/footer HTML,
optional SMTP host/port/user/pass override
- /admin/documenso API URL+key override, EOI Documenso template ID,
default EOI pathway (documenso-template vs inapp),
"Test connection" button
- /admin/branding logo URL, primary color, app name, email
header/footer HTML
- /admin/reminders port-level defaults for new interests +
port-wide daily-digest delivery window
- /admin/invitations send / list / resend / revoke CRM invitations
Per-user reminder digest
- /notifications/preferences gains a Reminder digest card:
immediate / daily / weekly / off, with HH:MM, day-of-week,
IANA timezone fields. Stored in user_profiles.preferences.reminders.
Plumbing
- port-config.ts typed accessors (getPortEmailConfig, getPortDocumensoConfig,
getPortBrandingConfig, getPortReminderConfig) — settings → env fallback.
- sendEmail accepts optional portId; resolves From/SMTP from settings
when supplied.
- documensoFetch + downloadSignedPdf accept optional portId; each public
function takes it through. checkDocumensoHealth() backs the test button.
- crm-invite.service gains listCrmInvites / revokeCrmInvite / resendCrmInvite
with audit-log entries (revoke_invite, resend_invite added to AuditAction).
- AdminLandingPage card grid + shared SettingsFormCard component to remove
per-page form boilerplate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:21:54 +02:00
|
|
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
|
|
|
});
|
|
|
|
|
return { ok: res.ok, status: res.status };
|
|
|
|
|
} catch (err) {
|
|
|
|
|
return { ok: false, error: err instanceof Error ? err.message : 'Unknown error' };
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-28 02:22:04 +02:00
|
|
|
|
|
|
|
|
// ─── Version-aware abstractions (Phase A PR2) ─────────────────────────────────
|
|
|
|
|
//
|
|
|
|
|
// Documenso v1.13 and v2.x diverge on field placement and document deletion:
|
|
|
|
|
//
|
|
|
|
|
// v1.13: per-field POST /api/v1/documents/{id}/fields with PIXEL coords;
|
|
|
|
|
// DELETE /api/v1/documents/{id} for void.
|
|
|
|
|
// v2.x: bulk POST /api/v2/envelope/field/create-many with PERCENT
|
|
|
|
|
// coords (0-100) and rich `fieldMeta`;
|
|
|
|
|
// DELETE /api/v2/envelope/{id} for void.
|
|
|
|
|
//
|
|
|
|
|
// Callers always work in PERCENT (0-100). For v1 the abstraction multiplies by
|
|
|
|
|
// the page dimensions returned by Documenso (cached per docId for the lifetime
|
2026-05-04 22:57:01 +02:00
|
|
|
// of the process - fields for a given doc usually go in a single batch).
|
2026-04-28 02:22:04 +02:00
|
|
|
|
|
|
|
|
export type DocumensoFieldType = 'SIGNATURE' | 'INITIALS' | 'DATE' | 'TEXT' | 'EMAIL';
|
|
|
|
|
|
|
|
|
|
export interface DocumensoFieldPlacement {
|
2026-05-04 22:57:01 +02:00
|
|
|
/** Documenso recipient id; v1 expects number, v2 string - coerced internally. */
|
2026-04-28 02:22:04 +02:00
|
|
|
recipientId: number | string;
|
|
|
|
|
type: DocumensoFieldType;
|
|
|
|
|
pageNumber: number;
|
|
|
|
|
/** All four are 0-100 percent of page dimensions. */
|
|
|
|
|
pageX: number;
|
|
|
|
|
pageY: number;
|
|
|
|
|
pageWidth: number;
|
|
|
|
|
pageHeight: number;
|
2026-05-04 22:57:01 +02:00
|
|
|
/** Optional v2 fieldMeta - passed through verbatim, ignored on v1. */
|
2026-04-28 02:22:04 +02:00
|
|
|
fieldMeta?: Record<string, unknown>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface DocumensoPageDimensions {
|
|
|
|
|
width: number;
|
|
|
|
|
height: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const DEFAULT_PAGE_DIMENSIONS: DocumensoPageDimensions = { width: 595, height: 842 }; // A4 pt
|
|
|
|
|
|
|
|
|
|
const pageDimensionCache = new Map<string, DocumensoPageDimensions>();
|
|
|
|
|
|
2026-05-04 22:57:01 +02:00
|
|
|
/** Test seam - clears the page-dimension memoization. */
|
2026-04-28 02:22:04 +02:00
|
|
|
export function __resetDocumensoCachesForTests(): void {
|
|
|
|
|
pageDimensionCache.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function getPageDimensions(docId: string, portId?: string): Promise<DocumensoPageDimensions> {
|
|
|
|
|
const cached = pageDimensionCache.get(docId);
|
|
|
|
|
if (cached) return cached;
|
|
|
|
|
// v1 doesn't expose page dimensions cleanly via the public API; the auto-
|
|
|
|
|
// placement use case is footer-anchored signature fields, where a default A4
|
|
|
|
|
// page rendered by Documenso is a safe assumption. Real page dims can be
|
|
|
|
|
// wired in a follow-up by parsing the document/document-data endpoints.
|
|
|
|
|
void portId;
|
|
|
|
|
pageDimensionCache.set(docId, DEFAULT_PAGE_DIMENSIONS);
|
|
|
|
|
return DEFAULT_PAGE_DIMENSIONS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Place one or more fields on a Documenso document. Coordinates are PERCENT
|
|
|
|
|
* (0-100) and converted to pixels for v1 internally.
|
|
|
|
|
*
|
|
|
|
|
* v1: dispatches one POST per field (no bulk endpoint).
|
|
|
|
|
* v2: single bulk POST.
|
|
|
|
|
*/
|
|
|
|
|
export async function placeFields(
|
|
|
|
|
docId: string,
|
|
|
|
|
fields: DocumensoFieldPlacement[],
|
|
|
|
|
portId?: string,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
if (fields.length === 0) return;
|
|
|
|
|
const { baseUrl, apiKey, apiVersion } = await resolveCreds(portId);
|
|
|
|
|
|
|
|
|
|
if (apiVersion === 'v2') {
|
|
|
|
|
const v2Fields = fields.map((f) => ({
|
|
|
|
|
recipientId: String(f.recipientId),
|
|
|
|
|
type: f.type,
|
|
|
|
|
pageNumber: f.pageNumber,
|
|
|
|
|
positionX: f.pageX,
|
|
|
|
|
positionY: f.pageY,
|
|
|
|
|
width: f.pageWidth,
|
|
|
|
|
height: f.pageHeight,
|
|
|
|
|
...(f.fieldMeta ? { fieldMeta: f.fieldMeta } : {}),
|
|
|
|
|
}));
|
|
|
|
|
// Note: v2 endpoint shape (envelopeId/recipientId types) must be
|
2026-05-04 22:57:01 +02:00
|
|
|
// confirmed against a live Documenso 2.x instance - see PR11 realapi
|
2026-04-28 02:22:04 +02:00
|
|
|
// suite. Spec risk register flags this drift as the top v2 risk.
|
fix(audit-tier-1): timeouts, lifecycle, per-port Documenso, FK constraints
Closes the second wave of HIGH-priority audit findings:
* fetchWithTimeout helper (new src/lib/fetch-with-timeout.ts) wraps
Documenso, OCR, currency, Umami, IMAP, etc. — a hung upstream can
no longer pin a worker concurrency slot indefinitely. OpenAI client
passes timeout: 30_000. ImapFlow gets socket / greeting / connection
timeouts.
* SIGTERM / SIGINT handler in src/server.ts drains in-flight HTTP,
closes Socket.io, and disconnects Redis before exit; compose
stop_grace_period bumped to 30s. Adds closeSocketServer() helper.
* env.ts gains zod-validated PORT and MULTI_NODE_DEPLOYMENT, and
filesystem.ts now reads from env (a typo can no longer silently
disable the multi-node guard).
* Per-port Documenso template + recipient IDs land in system_settings
with env fallback (PortDocumensoConfig now exposes eoiTemplateId,
clientRecipientId, developerRecipientId, approvalRecipientId).
document-templates.ts uses the per-port config and threads portId
into documensoGenerateFromTemplate().
* Migration 0042 wires the eleven HIGH-tier missing FK constraints
(documents/files/interests/reminders/berth_waiting_list/
form_submissions) plus polymorphic CHECK round 2
(yacht_ownership_history.owner_type, document_sends.document_kind),
invoices.billing_entity_id NOT EMPTY, and clients.merged_into self-FK.
Drizzle schema columns updated to .references(...) where possible
so the misleading "FK wired in relations.ts" comments are gone.
Test status: 1168/1168 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §§5,6,7,8,9,10 +
MED §§14,15,16,18.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:52:58 +02:00
|
|
|
const res = await fetchWithTimeout(`${baseUrl}/api/v2/envelope/field/create-many`, {
|
2026-04-28 02:22:04 +02:00
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${apiKey}`,
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ envelopeId: docId, fields: v2Fields }),
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const err = await res.text();
|
|
|
|
|
logger.error({ docId, status: res.status, err, portId }, 'Documenso v2 placeFields error');
|
|
|
|
|
throw new Error(`Documenso v2 placeFields error: ${res.status}`);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dims = await getPageDimensions(docId, portId);
|
|
|
|
|
for (const f of fields) {
|
|
|
|
|
const body = {
|
|
|
|
|
recipientId: typeof f.recipientId === 'string' ? Number(f.recipientId) : f.recipientId,
|
|
|
|
|
type: f.type,
|
|
|
|
|
pageNumber: f.pageNumber,
|
|
|
|
|
pageX: Math.round((f.pageX / 100) * dims.width),
|
|
|
|
|
pageY: Math.round((f.pageY / 100) * dims.height),
|
|
|
|
|
pageWidth: Math.round((f.pageWidth / 100) * dims.width),
|
|
|
|
|
pageHeight: Math.round((f.pageHeight / 100) * dims.height),
|
|
|
|
|
};
|
fix(audit-v3): platform-wide deferred-list cleanup (rounds 1-4)
Working through the audit-v2 deferred backlog. Each round was tested
(typecheck + 1168/1168 vitest) before moving on.
Round 1 — DB performance + AI cost visibility:
- Add missing FK indexes Postgres doesn't auto-create on
berth_reservations.{interest_id, contract_file_id},
documents.{file_id, signed_file_id}, document_events.signer_id,
document_templates.source_file_id, form_submissions.{form_template_id,
client_id}, document_sends.{brochure_id, brochure_version_id,
sent_by_user_id}. Without these, RESTRICT-checks on parent delete +
reverse-lookups walk the child tables fully. Migration 0037.
- AI worker now writes one ai_usage_ledger row per OpenAI call so admins
can audit spend per port/user/feature and future per-port budgets have
history to read from. Failure to write is logged-not-thrown so the
user-facing email draft is unaffected.
Round 2 — Boot-time + transport hardening:
- S3 backend verifies the bucket exists at startup (or auto-creates
when MINIO_AUTO_CREATE_BUCKET=true). A typo'd bucket name now
surfaces with a clear boot error instead of a vague Minio error
inside the first user-facing request.
- Documenso v1 placeFields: 3-attempt exponential-backoff retry on 5xx
+ network errors, fail-fast on 4xx. Stops one transient flake from
leaving a document with a partial field set.
- FilesystemBackend logs a structured warn-once at boot when the dev
HMAC fallback is in effect, so two processes started with different
BETTER_AUTH_SECRET values are observable (random 401s on file
downloads otherwise).
- Logger redact paths extended to cover *.headers.{authorization,
cookie}, *.config.headers.authorization, encrypted-credential blobs
(secretKeyEncrypted, smtpPassEncrypted, etc.), the Documenso
X-Documenso-Secret header, and 2-level nested forms.
Round 3 — UI feedback + permission gates:
- Storage admin migrate dialog: success toast with row count + error
toast on both dryRun and migrate mutations.
- Invoice detail Send + Record-payment buttons wrapped in
PermissionGate (invoices.send / invoices.record_payment); both
mutations now toast on success/error.
- Admin user list Edit button wrapped in PermissionGate(admin.manage_users).
- Scan-receipt page surfaces an amber warning when OCR fails so reps
know they can fill the form manually instead of staring at a stalled
spinner; the editable form now also opens on scanMutation.isError
/ uploadedFile, not only on success.
- Email threads list now renders skeleton rows during load + shared
EmptyState for the empty case (was a single "Loading…" line).
Round 4 — Service / route correctness:
- documentSends.sent_by_user_id was a free-text NOT NULL column with no
FK. Now nullable + FK to user(id) ON DELETE SET NULL so the audit row
survives a user being hard-deleted. Migration 0038 with a defensive
null-out for any orphan ids before attaching the constraint.
- Saved-views route: documented why withAuth alone is correct (the
service strictly filters by (portId, userId) — owner-only by design).
- Public-interests audit log: replaced "userId: null as unknown as
string" cast with userId: null; AuditLogParams already accepts null
for system-generated events.
- EOI in-app PDF fill: extracted setBerthRange() that, when the
AcroForm field is missing AND the context has a non-empty range
string, logs a structured warn so the deployment gap (live Documenso
template needs the field) is observable instead of silently dropping
the multi-berth range.
Test status: 1168/1168 vitest. tsc clean. Two new migrations
(0037/0038) need pnpm db:push (or migration apply) on the dev DB.
Deferred-doc updated with the remaining open items (bigger refactors).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 12:49:53 +02:00
|
|
|
// Retry transient failures so one flaky 5xx mid-loop doesn't leave
|
|
|
|
|
// the document with a partial field set. 3 attempts at 250 / 500 /
|
|
|
|
|
// 1000 ms; 4xx responses (validation errors) fail-fast.
|
|
|
|
|
let lastError: { status: number; body: string } | null = null;
|
|
|
|
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
fix(audit-tier-1): timeouts, lifecycle, per-port Documenso, FK constraints
Closes the second wave of HIGH-priority audit findings:
* fetchWithTimeout helper (new src/lib/fetch-with-timeout.ts) wraps
Documenso, OCR, currency, Umami, IMAP, etc. — a hung upstream can
no longer pin a worker concurrency slot indefinitely. OpenAI client
passes timeout: 30_000. ImapFlow gets socket / greeting / connection
timeouts.
* SIGTERM / SIGINT handler in src/server.ts drains in-flight HTTP,
closes Socket.io, and disconnects Redis before exit; compose
stop_grace_period bumped to 30s. Adds closeSocketServer() helper.
* env.ts gains zod-validated PORT and MULTI_NODE_DEPLOYMENT, and
filesystem.ts now reads from env (a typo can no longer silently
disable the multi-node guard).
* Per-port Documenso template + recipient IDs land in system_settings
with env fallback (PortDocumensoConfig now exposes eoiTemplateId,
clientRecipientId, developerRecipientId, approvalRecipientId).
document-templates.ts uses the per-port config and threads portId
into documensoGenerateFromTemplate().
* Migration 0042 wires the eleven HIGH-tier missing FK constraints
(documents/files/interests/reminders/berth_waiting_list/
form_submissions) plus polymorphic CHECK round 2
(yacht_ownership_history.owner_type, document_sends.document_kind),
invoices.billing_entity_id NOT EMPTY, and clients.merged_into self-FK.
Drizzle schema columns updated to .references(...) where possible
so the misleading "FK wired in relations.ts" comments are gone.
Test status: 1168/1168 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §§5,6,7,8,9,10 +
MED §§14,15,16,18.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:52:58 +02:00
|
|
|
const res = await fetchWithTimeout(`${baseUrl}/api/v1/documents/${docId}/fields`, {
|
fix(audit-v3): platform-wide deferred-list cleanup (rounds 1-4)
Working through the audit-v2 deferred backlog. Each round was tested
(typecheck + 1168/1168 vitest) before moving on.
Round 1 — DB performance + AI cost visibility:
- Add missing FK indexes Postgres doesn't auto-create on
berth_reservations.{interest_id, contract_file_id},
documents.{file_id, signed_file_id}, document_events.signer_id,
document_templates.source_file_id, form_submissions.{form_template_id,
client_id}, document_sends.{brochure_id, brochure_version_id,
sent_by_user_id}. Without these, RESTRICT-checks on parent delete +
reverse-lookups walk the child tables fully. Migration 0037.
- AI worker now writes one ai_usage_ledger row per OpenAI call so admins
can audit spend per port/user/feature and future per-port budgets have
history to read from. Failure to write is logged-not-thrown so the
user-facing email draft is unaffected.
Round 2 — Boot-time + transport hardening:
- S3 backend verifies the bucket exists at startup (or auto-creates
when MINIO_AUTO_CREATE_BUCKET=true). A typo'd bucket name now
surfaces with a clear boot error instead of a vague Minio error
inside the first user-facing request.
- Documenso v1 placeFields: 3-attempt exponential-backoff retry on 5xx
+ network errors, fail-fast on 4xx. Stops one transient flake from
leaving a document with a partial field set.
- FilesystemBackend logs a structured warn-once at boot when the dev
HMAC fallback is in effect, so two processes started with different
BETTER_AUTH_SECRET values are observable (random 401s on file
downloads otherwise).
- Logger redact paths extended to cover *.headers.{authorization,
cookie}, *.config.headers.authorization, encrypted-credential blobs
(secretKeyEncrypted, smtpPassEncrypted, etc.), the Documenso
X-Documenso-Secret header, and 2-level nested forms.
Round 3 — UI feedback + permission gates:
- Storage admin migrate dialog: success toast with row count + error
toast on both dryRun and migrate mutations.
- Invoice detail Send + Record-payment buttons wrapped in
PermissionGate (invoices.send / invoices.record_payment); both
mutations now toast on success/error.
- Admin user list Edit button wrapped in PermissionGate(admin.manage_users).
- Scan-receipt page surfaces an amber warning when OCR fails so reps
know they can fill the form manually instead of staring at a stalled
spinner; the editable form now also opens on scanMutation.isError
/ uploadedFile, not only on success.
- Email threads list now renders skeleton rows during load + shared
EmptyState for the empty case (was a single "Loading…" line).
Round 4 — Service / route correctness:
- documentSends.sent_by_user_id was a free-text NOT NULL column with no
FK. Now nullable + FK to user(id) ON DELETE SET NULL so the audit row
survives a user being hard-deleted. Migration 0038 with a defensive
null-out for any orphan ids before attaching the constraint.
- Saved-views route: documented why withAuth alone is correct (the
service strictly filters by (portId, userId) — owner-only by design).
- Public-interests audit log: replaced "userId: null as unknown as
string" cast with userId: null; AuditLogParams already accepts null
for system-generated events.
- EOI in-app PDF fill: extracted setBerthRange() that, when the
AcroForm field is missing AND the context has a non-empty range
string, logs a structured warn so the deployment gap (live Documenso
template needs the field) is observable instead of silently dropping
the multi-berth range.
Test status: 1168/1168 vitest. tsc clean. Two new migrations
(0037/0038) need pnpm db:push (or migration apply) on the dev DB.
Deferred-doc updated with the remaining open items (bigger refactors).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 12:49:53 +02:00
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${apiKey}`,
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
});
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
lastError = null;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
const errBody = await res.text().catch(() => '');
|
|
|
|
|
lastError = { status: res.status, body: errBody };
|
|
|
|
|
// Don't retry on 4xx — that's a validation error, won't change.
|
|
|
|
|
if (res.status >= 400 && res.status < 500) break;
|
|
|
|
|
// Backoff: 250ms, 500ms (skipped on the 3rd iteration because we exit).
|
|
|
|
|
if (attempt < 2) {
|
|
|
|
|
await new Promise((r) => setTimeout(r, 250 * Math.pow(2, attempt)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (lastError) {
|
|
|
|
|
logger.error(
|
|
|
|
|
{ docId, status: lastError.status, err: lastError.body, portId },
|
|
|
|
|
'Documenso v1 placeField error',
|
|
|
|
|
);
|
|
|
|
|
throw new Error(`Documenso v1 placeField error: ${lastError.status}`);
|
2026-04-28 02:22:04 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Auto-position one SIGNATURE field per recipient at the last-page footer,
|
|
|
|
|
* staggered horizontally so multiple signers don't overlap. Used by the
|
2026-05-04 22:57:01 +02:00
|
|
|
* upload-path wizard - admins can refine in Documenso afterwards.
|
2026-04-28 02:22:04 +02:00
|
|
|
*
|
|
|
|
|
* Layout (percent of page):
|
|
|
|
|
* y = 88 (footer band)
|
|
|
|
|
* height = 6
|
|
|
|
|
* width = min(20, 80 / N)
|
|
|
|
|
* x = i * (80/N) + (40 - 80/N * N / 2) (centered row)
|
|
|
|
|
*/
|
|
|
|
|
export async function placeDefaultSignatureFields(
|
|
|
|
|
docId: string,
|
|
|
|
|
recipients: Array<{ id: number | string; pageNumber: number }>,
|
|
|
|
|
portId?: string,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
if (recipients.length === 0) return;
|
|
|
|
|
const fields: DocumensoFieldPlacement[] = computeDefaultSignatureLayout(recipients);
|
|
|
|
|
await placeFields(docId, fields, portId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Pure function exported for unit testing layout math. */
|
|
|
|
|
export function computeDefaultSignatureLayout(
|
|
|
|
|
recipients: Array<{ id: number | string; pageNumber: number }>,
|
|
|
|
|
): DocumensoFieldPlacement[] {
|
|
|
|
|
const n = recipients.length;
|
|
|
|
|
if (n === 0) return [];
|
|
|
|
|
const slot = Math.min(20, 80 / n); // percent width per signer
|
|
|
|
|
const rowWidth = slot * n;
|
|
|
|
|
const startX = 50 - rowWidth / 2;
|
|
|
|
|
return recipients.map((r, i) => ({
|
|
|
|
|
recipientId: r.id,
|
|
|
|
|
type: 'SIGNATURE',
|
|
|
|
|
pageNumber: r.pageNumber,
|
|
|
|
|
pageX: Math.max(0, startX + i * slot),
|
|
|
|
|
pageY: 88,
|
|
|
|
|
pageWidth: slot,
|
|
|
|
|
pageHeight: 6,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Void/cancel a Documenso document.
|
|
|
|
|
*
|
|
|
|
|
* v1: DELETE /api/v1/documents/{id}
|
|
|
|
|
* v2: DELETE /api/v2/envelope/{id}
|
|
|
|
|
*
|
2026-05-04 22:57:01 +02:00
|
|
|
* Idempotent on 404 (already gone) - logs and resolves.
|
2026-04-28 02:22:04 +02:00
|
|
|
*/
|
|
|
|
|
export async function voidDocument(docId: string, portId?: string): Promise<void> {
|
|
|
|
|
const { baseUrl, apiKey, apiVersion } = await resolveCreds(portId);
|
|
|
|
|
const path = apiVersion === 'v2' ? `/api/v2/envelope/${docId}` : `/api/v1/documents/${docId}`;
|
fix(audit-tier-1): timeouts, lifecycle, per-port Documenso, FK constraints
Closes the second wave of HIGH-priority audit findings:
* fetchWithTimeout helper (new src/lib/fetch-with-timeout.ts) wraps
Documenso, OCR, currency, Umami, IMAP, etc. — a hung upstream can
no longer pin a worker concurrency slot indefinitely. OpenAI client
passes timeout: 30_000. ImapFlow gets socket / greeting / connection
timeouts.
* SIGTERM / SIGINT handler in src/server.ts drains in-flight HTTP,
closes Socket.io, and disconnects Redis before exit; compose
stop_grace_period bumped to 30s. Adds closeSocketServer() helper.
* env.ts gains zod-validated PORT and MULTI_NODE_DEPLOYMENT, and
filesystem.ts now reads from env (a typo can no longer silently
disable the multi-node guard).
* Per-port Documenso template + recipient IDs land in system_settings
with env fallback (PortDocumensoConfig now exposes eoiTemplateId,
clientRecipientId, developerRecipientId, approvalRecipientId).
document-templates.ts uses the per-port config and threads portId
into documensoGenerateFromTemplate().
* Migration 0042 wires the eleven HIGH-tier missing FK constraints
(documents/files/interests/reminders/berth_waiting_list/
form_submissions) plus polymorphic CHECK round 2
(yacht_ownership_history.owner_type, document_sends.document_kind),
invoices.billing_entity_id NOT EMPTY, and clients.merged_into self-FK.
Drizzle schema columns updated to .references(...) where possible
so the misleading "FK wired in relations.ts" comments are gone.
Test status: 1168/1168 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §§5,6,7,8,9,10 +
MED §§14,15,16,18.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:52:58 +02:00
|
|
|
const res = await fetchWithTimeout(`${baseUrl}${path}`, {
|
2026-04-28 02:22:04 +02:00
|
|
|
method: 'DELETE',
|
|
|
|
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
|
|
|
});
|
|
|
|
|
if (res.status === 404) {
|
|
|
|
|
logger.warn({ docId, portId }, 'Documenso voidDocument: already deleted');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const err = await res.text();
|
|
|
|
|
logger.error({ docId, status: res.status, err, portId }, 'Documenso voidDocument error');
|
|
|
|
|
throw new Error(`Documenso voidDocument error: ${res.status}`);
|
|
|
|
|
}
|
|
|
|
|
}
|