feat(portal-auth): URL fragment for activation/reset tokens
Step 8 per PRE-DEPLOY-PLAN § 1.2.5. Activation + password-reset links now carry the token in the URL fragment (`#token=…`) instead of the query string (`?token=…`). URL fragments are client-side only — the token never hits the server, never lands in proxy logs, never sits in the Referer header, and is invisible to upstream CDN/cache layers. The form still POSTs the token in the request body to authenticate. Changes: - portal-auth.service.ts URL builders for activation + reset switch to `#token=`. Inline comments cite the security rationale. - password-set-form.tsx reads the token via useSyncExternalStore so the SSR snapshot returns `null` and the client snapshot reads window.location.hash post-hydration (no set-state-in-effect Compiler violation). Helper prefers the fragment but falls back to the legacy `?token=` search param for the back-compat TTL window — so links sent before the switchover still work for their remaining lifetime. Component renders a "Loading…" placeholder during the pre-hydration null state. No DB changes; tokens themselves unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -144,7 +144,11 @@ async function issueActivationToken(
|
||||
const port = await db.query.ports.findFirst({ where: eq(ports.id, portId) });
|
||||
const portName = port?.name ?? 'Port Nimara';
|
||||
|
||||
const link = `${env.APP_URL}/portal/activate?token=${encodeURIComponent(raw)}`;
|
||||
// URL fragment (#token=…) instead of query string — keeps the
|
||||
// activation token out of server logs, proxy logs, Referer header,
|
||||
// and any CDN/edge cache. The portal /activate page reads the token
|
||||
// client-side via `window.location.hash`. See PRE-DEPLOY-PLAN § 1.2.5.
|
||||
const link = `${env.APP_URL}/portal/activate#token=${encodeURIComponent(raw)}`;
|
||||
const subjectOverride = await loadSubjectOverride(portId, 'portal_activation');
|
||||
const branding = await getBrandingShell(portId);
|
||||
const { subject, html, text } = await activationEmail(
|
||||
@@ -406,7 +410,9 @@ export async function requestPasswordReset(email: string): Promise<void> {
|
||||
|
||||
const port = await db.query.ports.findFirst({ where: eq(ports.id, user.portId) });
|
||||
const portName = port?.name ?? 'Port Nimara';
|
||||
const link = `${env.APP_URL}/portal/reset-password?token=${encodeURIComponent(raw)}`;
|
||||
// Same URL-fragment treatment as activation links — token never
|
||||
// travels server-side. See PRE-DEPLOY-PLAN § 1.2.5.
|
||||
const link = `${env.APP_URL}/portal/reset-password#token=${encodeURIComponent(raw)}`;
|
||||
const subjectOverride = await loadSubjectOverride(user.portId, 'portal_reset');
|
||||
const branding = await getBrandingShell(user.portId);
|
||||
const { subject, html, text } = await resetEmail(
|
||||
|
||||
Reference in New Issue
Block a user