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
@@ -0,0 +1,87 @@
|
||||
import { and, asc, eq, inArray, lt, sql } from 'drizzle-orm'
|
||||
import { Hono } from 'hono'
|
||||
import { z } from 'zod'
|
||||
import { hasRole } from '../../shared/enums.ts'
|
||||
import { actionListQuery, updateActionSchema } from '../../shared/schemas.ts'
|
||||
import { requireAuth } from '../auth/middleware.ts'
|
||||
import { correctiveActions, incidents, investigations, rootCauses } from '../db/schema.ts'
|
||||
import { diff } from '../lib/audit.ts'
|
||||
import { localDate } from '../lib/dates.ts'
|
||||
import { currentUser, fail, validate, type AppEnv } from '../lib/http.ts'
|
||||
import { assertActiveUser, auditIncident, selectActions, toAction } from '../lib/incidents.ts'
|
||||
|
||||
const idParam = z.object({ id: z.uuid() })
|
||||
const OPEN = ['open', 'in_progress'] as const
|
||||
|
||||
export const actionRoutes = new Hono<AppEnv>()
|
||||
.get('/', requireAuth, validate('query', actionListQuery), async (c) => {
|
||||
const me = currentUser(c)
|
||||
const { scope, filter } = c.req.valid('query')
|
||||
if (scope === 'all' && !hasRole(me.role, 'supervisor')) throw fail(403, 'You do not have permission to do that')
|
||||
const today = localDate()
|
||||
const rows = await selectActions(c.get('db'))
|
||||
.where(
|
||||
and(
|
||||
eq(incidents.organizationId, me.organizationId),
|
||||
scope === 'mine' ? eq(correctiveActions.assigneeId, me.id) : undefined,
|
||||
filter === 'open' ? inArray(correctiveActions.status, OPEN) : undefined,
|
||||
filter === 'overdue' ? and(inArray(correctiveActions.status, OPEN), lt(correctiveActions.dueDate, today)) : undefined,
|
||||
filter === 'done' ? inArray(correctiveActions.status, ['done', 'verified']) : undefined,
|
||||
),
|
||||
)
|
||||
.orderBy(sql`${correctiveActions.dueDate} asc nulls last`, asc(correctiveActions.createdAt))
|
||||
.limit(500)
|
||||
return c.json({ actions: rows.map((row) => toAction(row, today)) })
|
||||
})
|
||||
|
||||
.patch('/:id', requireAuth, validate('param', idParam), validate('json', updateActionSchema), async (c) => {
|
||||
const me = currentUser(c)
|
||||
const db = c.get('db')
|
||||
const [row] = await selectActions(db).where(and(eq(correctiveActions.id, c.req.valid('param').id), eq(incidents.organizationId, me.organizationId)))
|
||||
if (!row) throw fail(404, 'Action not found')
|
||||
const { action } = row
|
||||
const input = c.req.valid('json')
|
||||
|
||||
const isManager = hasRole(me.role, 'supervisor')
|
||||
const isAssignee = action.assigneeId === me.id
|
||||
if (!isManager) {
|
||||
// Assignees can report progress on their own actions, nothing else.
|
||||
const onlyStatus = Object.keys(input).every((key) => key === 'status')
|
||||
if (!isAssignee || !onlyStatus || (input.status && !['open', 'in_progress', 'done'].includes(input.status))) {
|
||||
throw fail(403, 'You can only update the progress of actions assigned to you')
|
||||
}
|
||||
}
|
||||
if (input.status === 'verified' && !hasRole(me.role, 'safety_manager')) throw fail(403, 'Only safety managers can verify actions')
|
||||
if (action.status === 'verified' && input.status && input.status !== 'verified' && !hasRole(me.role, 'safety_manager')) {
|
||||
throw fail(403, 'Only safety managers can reopen a verified action')
|
||||
}
|
||||
if (input.assigneeId) await assertActiveUser(db, me.organizationId, input.assigneeId)
|
||||
if (input.rootCauseId) {
|
||||
const [cause] = await db
|
||||
.select({ id: rootCauses.id })
|
||||
.from(rootCauses)
|
||||
.innerJoin(investigations, eq(investigations.id, rootCauses.investigationId))
|
||||
.where(and(eq(rootCauses.id, input.rootCauseId), eq(investigations.incidentId, action.incidentId)))
|
||||
if (!cause) throw fail(400, 'Root cause must belong to this incident')
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const status = input.status ?? action.status
|
||||
const update = {
|
||||
...input,
|
||||
completedAt: status === 'done' || status === 'verified' ? (action.completedAt ?? now) : null,
|
||||
verifiedAt: status === 'verified' ? (action.verifiedAt ?? now) : null,
|
||||
verifiedById: status === 'verified' ? (action.verifiedById ?? me.id) : null,
|
||||
// A new due date deserves a fresh reminder
|
||||
reminderSentOn: input.dueDate !== undefined && input.dueDate !== action.dueDate ? null : undefined,
|
||||
}
|
||||
const changes = diff(action, update)
|
||||
delete changes.reminderSentOn
|
||||
if (Object.keys(changes).length > 0) {
|
||||
await db.update(correctiveActions).set(update).where(eq(correctiveActions.id, action.id))
|
||||
await auditIncident(db, me, me.organizationId, action.incidentId, 'action.update', { actionId: action.id, title: action.title, ...changes })
|
||||
if (input.assigneeId && input.assigneeId !== action.assigneeId && input.assigneeId !== me.id) c.get('notifier').actionAssigned(action.id, me.name)
|
||||
}
|
||||
const [updated] = await selectActions(db).where(eq(correctiveActions.id, action.id))
|
||||
return c.json({ action: toAction(updated) })
|
||||
})
|
||||
Reference in new issue
Block a user