audit: Tier 0 quick wins — EMAIL_REDIRECT_TO prod guard + storage routing + metadata masking

Tier 0.2: src/lib/env.ts now refuses boot when NODE_ENV=production AND
EMAIL_REDIRECT_TO is set. Sendmail logs the rewrite at warn (was debug)
so dev/staging windows where someone forgets to unset are immediately
visible.

Tier 0.6: backup_jobs.storage_path added to TABLES_WITH_STORAGE_KEYS in
src/lib/storage/migrate.ts. Flipping the storage backend used to
silently orphan every pg_dump artefact — last-resort recovery path is
now actually portable.

Tier 1.7: createAuditLog now runs metadata through maskSensitiveFields
(was only applied to old/new value diffs). Portal-auth, crm-invite,
hard-delete and email-accounts services were writing raw emails into
this column unbounded.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 17:02:10 +02:00
parent a7b72801be
commit 0baca41693
13 changed files with 297 additions and 249 deletions

View File

@@ -106,10 +106,7 @@ export const GET = withAuth(
let baseline: RolePermissions | null = null;
if (!profile.isSuperAdmin) {
const portRole = await db.query.userPortRoles.findFirst({
where: and(
eq(userPortRoles.userId, targetUserId),
eq(userPortRoles.portId, portId),
),
where: and(eq(userPortRoles.userId, targetUserId), eq(userPortRoles.portId, portId)),
});
if (portRole) {
const role = await db.query.roles.findFirst({
@@ -171,10 +168,7 @@ export const PUT = withAuth(
// never apply, but it still consumes a unique slot and confuses
// future audits.
const targetPortRole = await db.query.userPortRoles.findFirst({
where: and(
eq(userPortRoles.userId, targetUserId),
eq(userPortRoles.portId, portId),
),
where: and(eq(userPortRoles.userId, targetUserId), eq(userPortRoles.portId, portId)),
});
if (!targetPortRole) {
throw new NotFoundError('User not assigned to this port');