feat: implement incident reporting and management system
- 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.
This commit is contained in:
1 parent
a382fa904a
commit
c69eccb5c1
47 files changed
+6297
-45
No files matched your search
@@ -0,0 +1,79 @@
|
||||
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 })
|
||||
})
|
||||
Reference in new issue
Block a user