Superadmin impersonation, hardened auth/upload paths, dependency updates

- Add superadmin impersonation: sessions.impersonated_by (migration 0003) is
  stamped onto audit rows so impersonated actions are attributable, with a
  persistent ImpersonationBanner in the app shell.
- Harden auth and upload handling across routes (safe redirect targets,
  filename sanitization, checkout grant handling).
- Update dependencies: Sentry 8 -> 10, @fastify/static 8 -> 10,
  react-router-dom 6.30.6; add find-my-way / fast-uri overrides.
- Add tests for safe-next, checkout-grant, and upload-filename.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leon Serfaty
2026-08-26 10:25:39 -04:00
co-authored by Claude Opus 5
parent 4a1122a7c9
commit eb36b81dc9
28 changed files with 7936 additions and 5763 deletions
+16 -5
View File
@@ -266,7 +266,9 @@ export async function adminRoutes(app: FastifyInstance) {
return updated;
});
// Impersonate: end the current session, start a new one for the target user.
// Impersonate: end the current session, start a short-lived one for the target user that is
// permanently stamped with the acting admin's id. Every audit row written from that session
// carries `impersonatedBy`, so support activity can never be mistaken for the customer's own.
app.post('/api/admin/users/:id/impersonate', async (req, reply) => {
const { id } = idParam.parse(req.params);
const db = getDb();
@@ -275,7 +277,11 @@ export async function adminRoutes(app: FastifyInstance) {
if (!target) return reply.code(404).send({ error: 'not_found' });
if (target.isSuspended) return reply.code(409).send({ error: 'target_suspended' });
if (target.id === req.user!.id) return reply.code(409).send({ error: 'cannot_impersonate_self' });
// Never let one platform admin borrow another's identity — that would launder an action
// between two accounts that both hold full platform authority.
if (target.isSuperadmin) return reply.code(409).send({ error: 'cannot_impersonate_superadmin' });
const adminId = req.user!.id;
const oldToken = req.cookies?.[SESSION_COOKIE];
if (oldToken) await destroySession(oldToken);
@@ -283,19 +289,24 @@ export async function adminRoutes(app: FastifyInstance) {
userId: target.id,
ip: req.ip,
userAgent: req.headers['user-agent'] ?? null,
impersonatedBy: adminId,
});
app.setSessionCookie(reply, token, expiresAt);
app.setCsrfCookie(reply, generateCsrfToken());
await logAudit({
userId: req.user!.id,
userId: adminId,
firmId: target.firmId,
action: 'admin.impersonate',
meta: { targetUserId: target.id, targetEmail: target.email },
action: 'admin.impersonate.start',
meta: { targetUserId: target.id, targetEmail: target.email, expiresAt: expiresAt.toISOString() },
ip: req.ip,
});
return { ok: true, impersonating: { id: target.id, email: target.email, firmId: target.firmId } };
return {
ok: true,
impersonating: { id: target.id, email: target.email, firmId: target.firmId },
expiresAt: expiresAt.toISOString(),
};
});
// ─────────────────────────── Contact inbox ───────────────────────────