feat: implement permissions and actions management
CI / Lint, typecheck, test, build (push) Canceled after 0s
CI / End-to-end tests (push) Canceled after 0s

- Add permissions logic for status options in corrective actions.
- Create tests for permissions functionality.
- Implement queries for user authentication and actions management.
- Develop Actions page to display and manage corrective actions.
- Create authentication pages for login, setup, and password reset.
- Add IncidentDetail page to view incident details and associated actions.
- Implement Settings page for user management and password changes.
This commit is contained in:
jayjennings committed 2026-10-06 11:20:44 -07:00
1 parent 708da6b9f7
commit 3f6dff5f24
94 files changed
+7213 -643

No files matched your search

+32 -7
View File
@@ -3,10 +3,11 @@ import { Hono } from 'hono'
import { z } from 'zod'
import { createUserSchema, updateUserSchema } from '../../shared/schemas.ts'
import { requireRole } from '../auth/middleware.ts'
import { createAuthToken } from '../auth/tokens.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 { organizations, users } from '../db/schema.ts'
import { audit, diff } from '../lib/audit.ts'
import { currentUser, fail, validate, type AppEnv } from '../lib/http.ts'
@@ -22,6 +23,7 @@ export const publicUser = {
}
const idParam = z.object({ id: z.uuid() })
const INVITE_TTL_MS = 7 * 24 * 60 * 60 * 1000
export const userRoutes = new Hono<AppEnv>()
.get('/', requireRole('safety_manager'), async (c) => {
@@ -30,6 +32,18 @@ export const userRoutes = new Hono<AppEnv>()
return c.json({ users: rows })
})
/** Active people who can lead investigations or be assigned actions. */
.get('/assignable', requireRole('supervisor'), async (c) => {
const me = currentUser(c)
const rows = await c
.get('db')
.select({ id: users.id, name: users.name, role: users.role })
.from(users)
.where(and(eq(users.organizationId, me.organizationId), eq(users.active, true)))
.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')
@@ -37,10 +51,15 @@ export const userRoutes = new Hono<AppEnv>()
try {
const [user] = await db
.insert(users)
.values({ ...input, organizationId: me.organizationId, passwordHash: await hashPassword(password) })
.values({ ...input, organizationId: me.organizationId, passwordHash: password ? await hashPassword(password) : null })
.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)
if (!password) {
const [organization] = await db.select({ name: organizations.name }).from(organizations).where(eq(organizations.id, me.organizationId))
const token = await createAuthToken(db, user.id, 'password_reset', INVITE_TTL_MS)
c.get('notifier').passwordReset(user.email, token, { organization: organization.name, invitedBy: me.name })
}
return c.json({ user, invited: !password }, 201)
} catch (error) {
if (isUniqueViolation(error)) throw fail(409, 'A user with that email already exists')
throw error
@@ -66,14 +85,20 @@ export const userRoutes = new Hono<AppEnv>()
if (otherAdmins.length === 0) throw fail(409, 'At least one active admin is required')
}
const changes = diff(target, input)
const { password, ...fields } = input
const changes: Record<string, unknown> = diff(target, fields)
if (password) changes.password = 'reset by admin'
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)
const [user] = await db
.update(users)
.set({ ...fields, ...(password ? { passwordHash: await hashPassword(password) } : {}) })
.where(eq(users.id, id))
.returning(publicUser)
// Roles are re-read on every request, so only deactivation and password resets need to end sessions.
if (input.active === false || (password && id !== me.id)) await deleteUserSessions(db, id)
await audit(db, { organizationId: me.organizationId, actorId: me.id, entityType: 'user', entityId: id, action: 'update', changes })
return c.json({ user })
})