Files
ims/app/api/admin/users/route.ts
T

184 lines
6.9 KiB
TypeScript

export const dynamic = 'force-dynamic'
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 } from '@/lib/db/with-user'
import { users } from '@/lib/db/schema'
import { writeAuditLog } from '@/lib/db/audit'
import { desc, eq, sql } from 'drizzle-orm'
export async function GET() {
const { session } = await requireAdmin()
if (!session) return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
const data = await asAdmin(db =>
db.select({
id: users.id,
name: users.name,
email: users.email,
phone: users.phone,
role: users.role,
department: users.department,
siteId: users.siteId,
active: users.active,
createdAt: users.createdAt,
})
.from(users)
.orderBy(desc(users.createdAt))
)
// Return snake_case to preserve existing frontend contract
return NextResponse.json(data.map(u => ({
id: u.id,
name: u.name,
email: u.email,
phone: u.phone,
role: u.role,
department: u.department,
site_id: u.siteId,
active: u.active,
created_at: u.createdAt,
})))
}
export async function POST(request: NextRequest) {
const { session } = await requireAdmin()
if (!session) return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
const body: { email?: string; name?: string; phone?: string; role?: string; site_id?: string; department?: string; password?: string } =
await request.json().catch(() => ({}))
const email = (body.email ?? '').trim().toLowerCase()
if (!email || !email.includes('@'))
return NextResponse.json({ error: 'Valid email required' }, { status: 422 })
if (body.role && !isValidRole(body.role))
return NextResponse.json({ error: 'Invalid role' }, { status: 422 })
const password = (body.password ?? '').trim()
if (!password)
return NextResponse.json({ error: 'Password required' }, { status: 422 })
if (password.length < 8)
return NextResponse.json({ error: 'Password must be at least 8 characters' }, { status: 422 })
const passwordHash = await hashPassword(password)
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,
})
})
})
if (!createdId) {
return NextResponse.json({ error: 'User creation failed' }, { status: 500 })
}
return NextResponse.json({ id: createdId }, { status: 201 })
}
export async function PATCH(request: NextRequest) {
const { session } = await requireAdmin()
if (!session) return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
const body: {
id?: string
role?: string
site_id?: string | null
active?: boolean
department?: string | null
} = await request.json().catch(() => ({}))
if (!body.id) return NextResponse.json({ error: 'id required' }, { status: 422 })
if (body.role !== undefined && !isValidRole(body.role))
return NextResponse.json({ error: 'Invalid role' }, { status: 422 })
if (body.id === session.sub && body.active === false)
return NextResponse.json({ error: 'Cannot deactivate your own account' }, { status: 422 })
if (body.id === session.sub && body.role !== undefined && body.role !== 'admin')
return NextResponse.json({ error: 'Cannot demote your own account' }, { status: 422 })
const drizzleUpdate: Partial<{ role: typeof users.$inferInsert['role']; siteId: string | null; active: boolean; department: string | null }> = {}
if (body.role !== undefined) drizzleUpdate.role = body.role as typeof users.$inferInsert['role']
if (body.site_id !== undefined) drizzleUpdate.siteId = body.site_id
if (body.active !== undefined) drizzleUpdate.active = body.active
if (body.department !== undefined) drizzleUpdate.department = body.department
if (Object.keys(drizzleUpdate).length === 0)
return NextResponse.json({ error: 'Nothing to update' }, { status: 422 })
const [before] = await asAdmin(db =>
db.select({ role: users.role, siteId: users.siteId, active: users.active, department: users.department })
.from(users)
.where(eq(users.id, body.id!))
.limit(1)
)
if (!before) return NextResponse.json({ error: 'User not found' }, { status: 404 })
try {
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 })
}
return NextResponse.json({ ok: true })
}
export async function DELETE(request: NextRequest) {
const { session } = await requireAdmin()
if (!session) return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
if (!id) return NextResponse.json({ error: 'id required' }, { status: 422 })
if (id === session.sub)
return NextResponse.json({ error: 'Cannot delete your own account' }, { status: 422 })
// fetch user info for audit before deletion
const [target] = await asAdmin(db =>
db.select({ email: users.email, name: users.name, role: users.role })
.from(users).where(eq(users.id, id)).limit(1)
)
// Delete user directly from DB
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 })
}
return NextResponse.json({ ok: true })
}