feat(portal): branded auth pages + legacy email styling + dev redirect override

- New PortalAuthShell component: blurred Port Nimara overhead background +
  circular logo + white rounded card, used by /portal/login,
  /portal/activate, /portal/reset-password
- New email/templates/portal-auth.ts: table-based, responsive (max-width
  600px / width 100%), matching the existing legacy inquiry templates;
  replaces the inline templates that lived in portal-auth.service
- EMAIL_REDIRECT_TO env override: when set, sendEmail routes every
  outbound message to that address regardless of recipient and tags the
  subject with "[redirected from <original>]". Dev/test safety net only;
  unset in production
- Portal password minimum length 12 → 9 (service + both API routes +
  client-side form)
- Dev helper script scripts/dev-trigger-portal-invite.ts: seeds a portal
  user against the first port-nimara client and uses EMAIL_REDIRECT_TO
  as the stored email so the tester can sign in with the address that
  received the activation mail

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-04-27 15:04:21 +02:00
parent c4085265ff
commit 4441f1177f
10 changed files with 396 additions and 201 deletions

View File

@@ -0,0 +1,59 @@
/**
* Dev-only helper: pick an existing client and trigger a portal-invite email.
* The activation email gets routed to EMAIL_REDIRECT_TO (set in .env) regardless
* of the per-portal-user `email` field — so we can use any throwaway address
* here without conflicting with seed data.
*
* Run: pnpm tsx scripts/dev-trigger-portal-invite.ts
*/
import 'dotenv/config';
import { db } from '@/lib/db';
import { clients } from '@/lib/db/schema/clients';
import { portalUsers } from '@/lib/db/schema/portal';
import { createPortalUser } from '@/lib/services/portal-auth.service';
import { env } from '@/lib/env';
import { eq } from 'drizzle-orm';
async function main(): Promise<void> {
if (!env.EMAIL_REDIRECT_TO) {
throw new Error(
'EMAIL_REDIRECT_TO is not set — refusing to send a real activation email to a real client.',
);
}
console.log(`EMAIL_REDIRECT_TO is set: ${env.EMAIL_REDIRECT_TO}`);
const client = await db.query.clients.findFirst({
where: eq(clients.portId, '294c8240-49a7-403e-92e8-fc3a524c00b4'),
});
if (!client) throw new Error('No client found in port-nimara');
// Use the redirect target as the portal user's actual email, so the
// tester can sign in with the same address that received the activation mail.
const portalEmail = env.EMAIL_REDIRECT_TO;
console.log(
`Creating portal user for client ${client.fullName} (${client.id}) with email ${portalEmail}`,
);
// Clear any prior dev-script seed so uniqueness checks don't trip.
await db.delete(portalUsers).where(eq(portalUsers.clientId, client.id));
await db.delete(portalUsers).where(eq(portalUsers.email, portalEmail));
const result = await createPortalUser({
clientId: client.id,
portId: client.portId,
email: portalEmail,
name: client.fullName,
createdBy: 'dev-script',
});
console.log('Portal user created:', result);
console.log(`Activation email enqueued — should arrive at ${portalEmail}.`);
process.exit(0);
}
main().catch((err) => {
console.error('Script failed:', err);
process.exit(1);
});