feat: implement permissions and actions management
- 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:
1 parent
708da6b9f7
commit
3f6dff5f24
94 files changed
+7213
-643
No files matched your search
+32
-7
@@ -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 })
|
||||
})
|
||||
Reference in new issue
Block a user