fix(audit): wrap admin + capa mutations with their audit writes in single transactions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 06:44:05 +08:00
co-authored by Claude Sonnet 4.6
parent 8acca41ca1
commit 9c67014bb9
5 changed files with 145 additions and 114 deletions
+50 -40
View File
@@ -4,10 +4,10 @@ import { NextRequest, NextResponse } from 'next/server'
import { isValidRole } from '@/lib/auth/roles'
import { requireAdmin } from '@/lib/auth/require-admin'
import { hashPassword } from '@/lib/auth/password'
import { asAdmin, withUser } from '@/lib/db/with-user'
import { asAdmin } from '@/lib/db/with-user'
import { users } from '@/lib/db/schema'
import { writeAuditLog } from '@/lib/db/audit'
import { desc, eq } from 'drizzle-orm'
import { desc, eq, sql } from 'drizzle-orm'
export async function GET() {
const { session } = await requireAdmin()
@@ -61,28 +61,33 @@ export async function POST(request: NextRequest) {
const passwordHash = await hashPassword(password)
const [created] = await asAdmin(db => db.insert(users).values({
name: body.name ?? '',
email,
phone: (body.phone ?? '').trim() || null,
role: (body.role as typeof users.$inferInsert['role']) ?? 'reporter',
siteId: body.site_id ?? null,
department: body.department ?? null,
passwordHash,
emailVerifiedAt: new Date(),
}).returning({ id: users.id }))
if (!created) {
return NextResponse.json({ error: 'User creation failed' }, { status: 500 })
}
await withUser(session.sub, async tx => {
await writeAuditLog(tx, 'users', created.id, 'created', {
email, role: body.role ?? 'reporter', site_id: body.site_id ?? null,
let createdId: string | undefined
await asAdmin(async db => {
await db.transaction(async tx => {
await tx.execute(sql`SELECT set_config('app.user_id', ${session.sub}, true)`)
const [created] = await tx.insert(users).values({
name: body.name ?? '',
email,
phone: (body.phone ?? '').trim() || null,
role: (body.role as typeof users.$inferInsert['role']) ?? 'reporter',
siteId: body.site_id ?? null,
department: body.department ?? null,
passwordHash,
emailVerifiedAt: new Date(),
}).returning({ id: users.id })
if (!created) throw new Error('User creation failed')
createdId = created.id
await writeAuditLog(tx, 'users', created.id, 'created', {
email, role: body.role ?? 'reporter', site_id: body.site_id ?? null,
})
})
})
return NextResponse.json({ id: created.id }, { status: 201 })
if (!createdId) {
return NextResponse.json({ error: 'User creation failed' }, { status: 500 })
}
return NextResponse.json({ id: createdId }, { status: 201 })
}
export async function PATCH(request: NextRequest) {
@@ -122,17 +127,20 @@ export async function PATCH(request: NextRequest) {
if (!before) return NextResponse.json({ error: 'User not found' }, { status: 404 })
try {
await asAdmin(db => db.update(users).set(drizzleUpdate).where(eq(users.id, body.id!)))
await asAdmin(async db => {
await db.transaction(async tx => {
await tx.execute(sql`SELECT set_config('app.user_id', ${session.sub}, true)`)
await tx.update(users).set(drizzleUpdate).where(eq(users.id, body.id!))
await writeAuditLog(
tx, 'users', body.id!, 'admin_update',
drizzleUpdate,
{ role: before.role, site_id: before.siteId, active: before.active, department: before.department },
)
})
})
} catch {
return NextResponse.json({ error: 'Update failed' }, { status: 500 })
}
await withUser(session.sub, async tx => {
await writeAuditLog(
tx, 'users', body.id!, 'admin_update',
drizzleUpdate,
{ role: before.role, site_id: before.siteId, active: before.active, department: before.department },
)
})
return NextResponse.json({ ok: true })
}
@@ -153,21 +161,23 @@ export async function DELETE(request: NextRequest) {
)
// Delete user directly from DB
const [deleted] = await asAdmin(db =>
db.delete(users).where(eq(users.id, id)).returning({ id: users.id })
)
let deleted: { id: string } | undefined
await asAdmin(async db => {
await db.transaction(async tx => {
await tx.execute(sql`SELECT set_config('app.user_id', ${session.sub}, true)`)
const [result] = await tx.delete(users).where(eq(users.id, id)).returning({ id: users.id })
deleted = result
if (result && target) {
await writeAuditLog(tx, 'users', id, 'deleted', {
email: target.email, name: target.name, role: target.role,
})
}
})
})
if (!deleted) {
return NextResponse.json({ error: 'User not found or deletion failed' }, { status: 500 })
}
if (target) {
await withUser(session.sub, async tx => {
await writeAuditLog(tx, 'users', id, 'deleted', {
email: target.email, name: target.name, role: target.role,
})
})
}
return NextResponse.json({ ok: true })
}