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:
@@ -53,10 +53,7 @@ export async function POST(req: NextRequest) {
|
||||
const ip = clientIp(req);
|
||||
const rl = await checkRateLimit(ip, rateLimiters.auth);
|
||||
if (!rl.allowed) {
|
||||
return NextResponse.json(
|
||||
{ email: '' },
|
||||
{ status: 429, headers: rateLimitHeaders(rl) },
|
||||
);
|
||||
return NextResponse.json({ email: '' }, { status: 429, headers: rateLimitHeaders(rl) });
|
||||
}
|
||||
|
||||
const body = (await req.json().catch(() => ({}))) as { identifier?: string };
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -21,12 +21,7 @@ const updateProfileSchema = z.object({
|
||||
* Uniqueness is checked below before the UPDATE — collisions surface
|
||||
* as a 409 with a friendly message.
|
||||
*/
|
||||
username: z
|
||||
.union([
|
||||
z.string().transform((s) => s.trim().toLowerCase()),
|
||||
z.null(),
|
||||
])
|
||||
.optional(),
|
||||
username: z.union([z.string().transform((s) => s.trim().toLowerCase()), z.null()]).optional(),
|
||||
phone: z.string().nullable().optional(),
|
||||
// Refuse `javascript:` / `data:` schemes — z.string().url() lets them
|
||||
// through and `<a href={avatarUrl}>` would otherwise be a stored-XSS
|
||||
|
||||
Reference in New Issue
Block a user