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:
52
src/app/api/public/files/[id]/route.ts
Normal file
52
src/app/api/public/files/[id]/route.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { NextResponse, type NextRequest } from 'next/server';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
import { files } from '@/lib/db/schema/documents';
|
||||
import { getStorageBackend } from '@/lib/storage';
|
||||
import { errorResponse, NotFoundError } from '@/lib/errors';
|
||||
|
||||
/**
|
||||
* Public, unauthenticated stream-by-id for branding assets only. Used by
|
||||
* outbound email templates and the branded auth shell — surfaces where
|
||||
* the consumer can't authenticate (an inbox image fetch has no session
|
||||
* cookie). The `category = 'branding'` gate ensures only assets the
|
||||
* admin explicitly uploaded as port branding leak through this surface;
|
||||
* every other category (eoi, contract, receipt, …) keeps its
|
||||
* authenticated `/api/v1/files/[id]/preview` path.
|
||||
*
|
||||
* Cached for a day at the edge. Admins replacing a logo write a new
|
||||
* file id (the system_settings URL changes), so a stale CDN entry for
|
||||
* the old id is harmless.
|
||||
*/
|
||||
export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await ctx.params;
|
||||
const row = await db.query.files.findFirst({ where: eq(files.id, id) });
|
||||
if (!row || row.category !== 'branding') {
|
||||
throw new NotFoundError('File');
|
||||
}
|
||||
|
||||
const backend = await getStorageBackend();
|
||||
const stream = await backend.get(row.storagePath);
|
||||
|
||||
// Convert the Node Readable into a Web ReadableStream so NextResponse
|
||||
// can hand it back without buffering the whole blob in memory.
|
||||
const webStream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
stream.on('data', (chunk: Buffer) => controller.enqueue(new Uint8Array(chunk)));
|
||||
stream.on('end', () => controller.close());
|
||||
stream.on('error', (err) => controller.error(err));
|
||||
},
|
||||
});
|
||||
|
||||
return new NextResponse(webStream, {
|
||||
headers: {
|
||||
'Content-Type': row.mimeType ?? 'application/octet-stream',
|
||||
'Cache-Control': 'public, max-age=86400, immutable',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user