feat(branding): multi-tenant brand naming + per-port email shell + auth UI continuity
Removes the last hardcoded "Port Nimara" references so a tenant cloning
the deploy with a fresh slug sees their own brand throughout.
Browser + native chrome:
- `generateMetadata` reads `branding_app_name` from the first port row
so the browser tab title, apple-web-app title, and template literal
reflect the tenant (fallback "CRM" until DB is seeded).
- Mobile topbar derives the brand-mark initials from the port slug
("port-nimara" → "PN", "marina-alpha" → "MA") — no code edit on clone.
- `documenso-payload` default redirect URL is `""` so Documenso falls
back to its own post-sign page instead of routing every tenant's
signers to portnimara.com; per-port `redirectUrl` setting still wins.
- Server-startup log uses generic "CRM server listening".
Email + auth shell:
- New `auth-shell-branding.ts` resolves logo / background / appName once
per request from `system_settings`; used by both the email shell and
the auth-pages SSR layout.
- `auth-branding-provider` wraps `/login`, `/reset-password`, `/set-password`,
portal `/portal/*` so the branded shell hydrates with the same assets
the inbox sees.
- `me/email` change email uses the branded shell instead of inline HTML
with "Port Nimara CRM" baked into copy.
- Admin branding page adds an email-preview card (POSTs to
`/api/v1/admin/branding/email-preview`) so an admin can spot-check
their templates before going live.
- `/api/public/files/[id]` exposes branding-category files anonymously
so inbox images (no session cookie) can render; any other category
still flows through authenticated `/api/v1/files/[id]/preview`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -89,19 +89,49 @@ export const PATCH = withAuth(async (req, ctx) => {
|
||||
const cancelUrl = `${baseUrl}/api/v1/me/email/cancel/${rawToken}`;
|
||||
|
||||
try {
|
||||
const { sendEmail } = await import('@/lib/email');
|
||||
const [{ sendEmail }, { renderShell, safeUrl }, { resolveAuthShellBranding }] =
|
||||
await Promise.all([
|
||||
import('@/lib/email'),
|
||||
import('@/lib/email/shell'),
|
||||
import('@/lib/email/auth-shell-branding'),
|
||||
]);
|
||||
const branding = await resolveAuthShellBranding();
|
||||
const appName = branding?.appName?.trim() || 'CRM';
|
||||
const brandingShell = branding
|
||||
? {
|
||||
logoUrl: branding.logoUrl,
|
||||
backgroundUrl: branding.backgroundUrl,
|
||||
primaryColor: null,
|
||||
emailHeaderHtml: null,
|
||||
emailFooterHtml: null,
|
||||
}
|
||||
: null;
|
||||
const safeOldEmail = ctx.user.email.replace(/[<>&]/g, '');
|
||||
const safeNewEmail = email.replace(/[<>&]/g, '');
|
||||
const confirmBody = `
|
||||
<p style="margin-bottom:16px;">Hi,</p>
|
||||
<p style="margin-bottom:16px;">You (or someone using your account) requested to change the sign-in email on your ${appName} account from <strong>${safeOldEmail}</strong> to <strong>${safeNewEmail}</strong>.</p>
|
||||
<p style="margin-bottom:16px;"><a href="${safeUrl(confirmUrl)}" style="color:#2563eb;font-weight:600;">Click here to confirm this change</a> — the link expires in ${VERIFY_TOKEN_TTL_MINUTES} minutes.</p>
|
||||
<p style="color:#64748b;">If you didn't request this, ignore this email.</p>
|
||||
`;
|
||||
const cancelBody = `
|
||||
<p style="margin-bottom:16px;">Hi,</p>
|
||||
<p style="margin-bottom:16px;">A change to your sign-in email was requested. If this wasn't you, <a href="${safeUrl(cancelUrl)}" style="color:#2563eb;font-weight:600;">click here to cancel the change</a> immediately and consider rotating your password.</p>
|
||||
`;
|
||||
const confirmSubject = `Confirm your new ${appName} email address`;
|
||||
const noticeSubject = `A change to your ${appName} email was requested`;
|
||||
await Promise.allSettled([
|
||||
sendEmail(
|
||||
email,
|
||||
'Confirm your new Port Nimara CRM email address',
|
||||
`<p>Hi,</p><p>You (or someone using your account) requested to change the sign-in email on your Port Nimara CRM account from <strong>${ctx.user.email}</strong> to <strong>${email}</strong>.</p><p><a href="${confirmUrl}">Click here to confirm this change</a> — the link expires in ${VERIFY_TOKEN_TTL_MINUTES} minutes.</p><p>If you didn't request this, ignore this email.</p>`,
|
||||
confirmSubject,
|
||||
renderShell({ title: confirmSubject, body: confirmBody, branding: brandingShell }),
|
||||
undefined,
|
||||
`Confirm new email: ${confirmUrl}`,
|
||||
),
|
||||
sendEmail(
|
||||
ctx.user.email,
|
||||
'A change to your Port Nimara CRM email was requested',
|
||||
`<p>Hi,</p><p>A change to your sign-in email was requested. If this wasn't you, <a href="${cancelUrl}">click here to cancel the change</a> immediately and consider rotating your password.</p>`,
|
||||
noticeSubject,
|
||||
renderShell({ title: noticeSubject, body: cancelBody, branding: brandingShell }),
|
||||
undefined,
|
||||
`Cancel email change: ${cancelUrl}`,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user