fix(auth): harden admin gate, X-Port-Id, portal JWT, saved-views

- Add server-side `<admin>/layout.tsx` that redirects non-super-admins to
  `/[portSlug]/dashboard`. Closes the gap where any authed user could
  guess the URL and reach Users / Roles / Audit Log / Backup.
- `withAuth` super-admin branch now 404s when the requested portId does
  not match a real port row, preventing a compromised super-admin
  session from operating against a fabricated portId.
- Portal JWTs now carry `aud: 'portal'` + `iss: 'pn-crm'` claims and
  `verifyPortalToken` requires both, so a portal token can no longer be
  replayed against the CRM session path or vice versa. In-flight tokens
  (≤24h) will be invalidated once on deploy.
- `saved-views/[id]` PATCH and DELETE now do an explicit ownership
  check before the service call, returning 403 instead of relying on
  the service's internal userId filter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-05-02 23:00:42 +02:00
parent a767652d74
commit 6af2ac9680
4 changed files with 79 additions and 2 deletions

View File

@@ -181,10 +181,15 @@ export function withAuth(
}
} else if (profile.isSuperAdmin && portId) {
// Super admin still needs portSlug for response context.
// We also validate the portId actually exists — a super-admin session
// must not be able to operate against a fabricated portId.
const port = await db.query.ports.findFirst({
where: eq(ports.id, portId),
});
portSlug = port?.slug ?? '';
if (!port) {
return NextResponse.json({ error: 'Port not found' }, { status: 404 });
}
portSlug = port.slug;
}
const ctx: AuthContext = {