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>
This commit is contained in:
@@ -2,9 +2,11 @@ import nodemailer, { type Transporter } from 'nodemailer';
|
||||
|
||||
import { env } from '@/lib/env';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { getPortEmailConfig, type PortEmailConfig } from '@/lib/services/port-config';
|
||||
|
||||
/**
|
||||
* Creates and returns a new Nodemailer SMTP transporter.
|
||||
* Creates and returns a new Nodemailer SMTP transporter using env defaults.
|
||||
* For port-scoped configuration use {@link createPortTransporter} instead.
|
||||
*
|
||||
* A new instance is created on each call so the factory can be used in
|
||||
* contexts where connection pooling is managed externally (e.g. per-request
|
||||
@@ -22,11 +24,23 @@ export function createTransporter(): Transporter {
|
||||
});
|
||||
}
|
||||
|
||||
function createTransporterFromConfig(cfg: PortEmailConfig): Transporter {
|
||||
return nodemailer.createTransport({
|
||||
host: cfg.smtpHost,
|
||||
port: cfg.smtpPort,
|
||||
secure: cfg.smtpPort === 465,
|
||||
...(cfg.smtpUser && cfg.smtpPass ? { auth: { user: cfg.smtpUser, pass: cfg.smtpPass } } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
export interface SendEmailOptions {
|
||||
to: string | string[];
|
||||
subject: string;
|
||||
html: string;
|
||||
from?: string;
|
||||
/** When provided, port-level email settings override env defaults. */
|
||||
portId?: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,8 +56,10 @@ export async function sendEmail(
|
||||
html: string,
|
||||
from?: string,
|
||||
text?: string,
|
||||
portId?: string,
|
||||
): Promise<nodemailer.SentMessageInfo> {
|
||||
const transporter = createTransporter();
|
||||
const cfg = portId ? await getPortEmailConfig(portId) : null;
|
||||
const transporter = cfg ? createTransporterFromConfig(cfg) : createTransporter();
|
||||
|
||||
const requestedTo = Array.isArray(to) ? to.join(', ') : to;
|
||||
const effectiveTo = env.EMAIL_REDIRECT_TO ?? requestedTo;
|
||||
@@ -51,16 +67,23 @@ export async function sendEmail(
|
||||
? `[redirected from ${requestedTo}] ${subject}`
|
||||
: subject;
|
||||
|
||||
const fromHeader =
|
||||
from ??
|
||||
(cfg ? `${cfg.fromName} <${cfg.fromAddress}>` : undefined) ??
|
||||
env.SMTP_FROM ??
|
||||
`Port Nimara CRM <noreply@${env.SMTP_HOST}>`;
|
||||
|
||||
const info = await transporter.sendMail({
|
||||
from: from ?? env.SMTP_FROM ?? `Port Nimara CRM <noreply@${env.SMTP_HOST}>`,
|
||||
from: fromHeader,
|
||||
to: effectiveTo,
|
||||
subject: effectiveSubject,
|
||||
html,
|
||||
...(cfg?.replyTo ? { replyTo: cfg.replyTo } : {}),
|
||||
...(text ? { text } : {}),
|
||||
});
|
||||
|
||||
logger.debug(
|
||||
{ messageId: info.messageId, to: effectiveTo, originalTo: requestedTo, subject },
|
||||
{ messageId: info.messageId, to: effectiveTo, originalTo: requestedTo, subject, portId },
|
||||
env.EMAIL_REDIRECT_TO ? 'Email sent (redirected)' : 'Email sent',
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user