- Add incident reporting routes and logic to handle creation, updating, and retrieval of incidents. - Implement visibility rules for users based on roles (reporters, supervisors). - Create tests for incident reporting functionality to ensure correctness and reliability. - Introduce site and area management routes, including validation for area hierarchy. - Establish user management routes with role-based access control for creating and updating users. - Add setup routes for initializing the application with an organization and admin user. - Implement shared enums and schemas for consistent validation and data handling across the application. - Create a test helper for setting up an in-memory database and API client for testing. - Add TypeScript configuration for server-side code with strict type checking and module resolution.
80 lines
3.5 KiB
TypeScript
80 lines
3.5 KiB
TypeScript
import { and, asc, eq, ne } from 'drizzle-orm'
|
|
import { Hono } from 'hono'
|
|
import { z } from 'zod'
|
|
import { createUserSchema, updateUserSchema } from '../../shared/schemas.ts'
|
|
import { requireRole } from '../auth/middleware.ts'
|
|
import { hashPassword } from '../auth/password.ts'
|
|
import { deleteUserSessions } from '../auth/sessions.ts'
|
|
import { isUniqueViolation } from '../db/client.ts'
|
|
import { users } from '../db/schema.ts'
|
|
import { audit, diff } from '../lib/audit.ts'
|
|
import { currentUser, fail, validate, type AppEnv } from '../lib/http.ts'
|
|
|
|
/** Columns safe to return to clients (never the password hash). */
|
|
export const publicUser = {
|
|
id: users.id,
|
|
email: users.email,
|
|
name: users.name,
|
|
role: users.role,
|
|
active: users.active,
|
|
lastLoginAt: users.lastLoginAt,
|
|
createdAt: users.createdAt,
|
|
}
|
|
|
|
const idParam = z.object({ id: z.uuid() })
|
|
|
|
export const userRoutes = new Hono<AppEnv>()
|
|
.get('/', requireRole('safety_manager'), async (c) => {
|
|
const me = currentUser(c)
|
|
const rows = await c.get('db').select(publicUser).from(users).where(eq(users.organizationId, me.organizationId)).orderBy(asc(users.name))
|
|
return c.json({ users: rows })
|
|
})
|
|
|
|
.post('/', requireRole('admin'), validate('json', createUserSchema), async (c) => {
|
|
const me = currentUser(c)
|
|
const { password, ...input } = c.req.valid('json')
|
|
const db = c.get('db')
|
|
try {
|
|
const [user] = await db
|
|
.insert(users)
|
|
.values({ ...input, organizationId: me.organizationId, passwordHash: await hashPassword(password) })
|
|
.returning(publicUser)
|
|
await audit(db, { organizationId: me.organizationId, actorId: me.id, entityType: 'user', entityId: user.id, action: 'create', changes: { email: user.email, role: user.role } })
|
|
return c.json({ user }, 201)
|
|
} catch (error) {
|
|
if (isUniqueViolation(error)) throw fail(409, 'A user with that email already exists')
|
|
throw error
|
|
}
|
|
})
|
|
|
|
.patch('/:id', requireRole('admin'), validate('param', idParam), validate('json', updateUserSchema), async (c) => {
|
|
const me = currentUser(c)
|
|
const { id } = c.req.valid('param')
|
|
const input = c.req.valid('json')
|
|
const db = c.get('db')
|
|
|
|
const [target] = await db.select().from(users).where(and(eq(users.id, id), eq(users.organizationId, me.organizationId)))
|
|
if (!target) throw fail(404, 'User not found')
|
|
if (target.id === me.id && input.active === false) throw fail(400, 'You cannot deactivate your own account')
|
|
|
|
const losesAdmin = target.role === 'admin' && target.active && ((input.role && input.role !== 'admin') || input.active === false)
|
|
if (losesAdmin) {
|
|
const otherAdmins = await db
|
|
.select({ id: users.id })
|
|
.from(users)
|
|
.where(and(eq(users.organizationId, me.organizationId), eq(users.role, 'admin'), eq(users.active, true), ne(users.id, target.id)))
|
|
if (otherAdmins.length === 0) throw fail(409, 'At least one active admin is required')
|
|
}
|
|
|
|
const changes = diff(target, input)
|
|
if (Object.keys(changes).length === 0) {
|
|
const [user] = await db.select(publicUser).from(users).where(eq(users.id, id))
|
|
return c.json({ user })
|
|
}
|
|
const [user] = await db.update(users).set(input).where(eq(users.id, id)).returning(publicUser)
|
|
// Roles are re-read on every request, so only deactivation needs to end sessions.
|
|
if (input.active === false) await deleteUserSessions(db, id)
|
|
await audit(db, { organizationId: me.organizationId, actorId: me.id, entityType: 'user', entityId: id, action: 'update', changes })
|
|
return c.json({ user })
|
|
})
|