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
+70
-16
@@ -1,11 +1,13 @@
|
||||
# API reference
|
||||
|
||||
JSON over HTTP under `/api`. Authentication uses a session cookie (`safetylog_session`, http-only) set by setup, sign-in and password change.
|
||||
JSON over HTTP under `/api`. Authentication uses a session cookie (`safetylog_session`, http-only) set by setup, sign-in, emailed links and password changes.
|
||||
|
||||
Request bodies are validated against the schemas in [`shared/schemas.ts`](../shared/schemas.ts). Errors look like `{ "error": "message" }`; validation errors (400) also include `issues: [{ path, message }]`.
|
||||
Request bodies are validated against [`shared/schemas.ts`](../shared/schemas.ts); response shapes are in [`shared/api-types.ts`](../shared/api-types.ts). Errors look like `{ "error": "message" }`; validation errors (400) also include `issues: [{ path, message }]`.
|
||||
|
||||
Roles, from least to most privileged: `reporter` < `supervisor` < `safety_manager` < `admin`. A role can do everything the roles below it can.
|
||||
|
||||
Browsers must send an `Origin` header matching the site on form/multipart and body-less requests (all browsers do); JSON requests are protected by CORS.
|
||||
|
||||
## Setup and authentication
|
||||
|
||||
| Method | Path | Who | Notes |
|
||||
@@ -14,19 +16,24 @@ Roles, from least to most privileged: `reporter` < `supervisor` < `safety_manage
|
||||
| GET | `/api/setup` | anyone | `{ needsSetup }`: true until the first admin exists |
|
||||
| POST | `/api/setup` | anyone, once | `{ organizationName, name, email, password }`. Creates the organization and an admin, signs them in. 409 afterwards |
|
||||
| POST | `/api/auth/login` | anyone | `{ email, password }`. 401 on bad credentials, 429 after 10 attempts per email in 15 minutes |
|
||||
| POST | `/api/auth/magic-link` | anyone | `{ email }`. Emails a single-use sign-in link (15 minutes). Always 204 |
|
||||
| POST | `/api/auth/magic-link/verify` | anyone | `{ token }`. Signs in |
|
||||
| POST | `/api/auth/password-reset` | anyone | `{ email }`. Emails a reset link (1 hour). Always 204 |
|
||||
| POST | `/api/auth/password-reset/confirm` | anyone | `{ token, newPassword }`. Sets the password, signs out other sessions, signs in. Also completes invitations |
|
||||
| POST | `/api/auth/logout` | anyone | Ends the session. 204 |
|
||||
| GET | `/api/auth/me` | signed in | `{ user, organization }` |
|
||||
| POST | `/api/auth/password` | signed in | `{ currentPassword, newPassword }`. Signs out all other sessions. 204 |
|
||||
|
||||
Passwords need at least 10 characters.
|
||||
Passwords need at least 10 characters. Email requests are limited to 5 per address per 15 minutes. Emailed links land on web pages (`/auth/magic`, `/auth/reset`) that POST the token, so link scanners in email systems can't use them up.
|
||||
|
||||
## Users
|
||||
|
||||
| Method | Path | Who | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| GET | `/api/users` | safety_manager | All users in the organization |
|
||||
| POST | `/api/users` | admin | `{ name, email, role, password }`. 409 if the email exists |
|
||||
| PATCH | `/api/users/:id` | admin | `{ name?, role?, active? }`. Deactivating signs the user out. The last active admin can't be demoted or deactivated |
|
||||
| GET | `/api/users/assignable` | supervisor | Active users `{ id, name, role }`, for assigning leads and actions |
|
||||
| POST | `/api/users` | admin | `{ name, email, role, password? }`. Without `password` an invitation email is sent (link valid 7 days). Returns `{ user, invited }` |
|
||||
| PATCH | `/api/users/:id` | admin | `{ name?, role?, active?, password? }`. Deactivating or setting a password signs the user out. The last active admin can't be demoted or deactivated |
|
||||
|
||||
## Sites and areas
|
||||
|
||||
@@ -35,21 +42,31 @@ Passwords need at least 10 characters.
|
||||
| GET | `/api/sites` | signed in | Sites with their `areas` (flat list; build the tree from `parentId`). `?includeArchived=true` to include archived |
|
||||
| POST | `/api/sites` | safety_manager | `{ name, address?, timezone? }` |
|
||||
| PATCH | `/api/sites/:id` | safety_manager | `{ name?, address?, timezone?, archived? }` |
|
||||
| POST | `/api/sites/:id/areas` | safety_manager | `{ name, kind?, parentId? }`. `kind`: `building`, `area` (default), `line`, `other`. Parent must be in the same site |
|
||||
| POST | `/api/sites/:id/areas` | safety_manager | `{ name, kind?, parentId? }`. `kind`: `building`, `area` (default), `line`, `other` |
|
||||
| PATCH | `/api/areas/:id` | safety_manager | `{ name?, kind?, archived? }` |
|
||||
|
||||
## Incidents
|
||||
|
||||
Incidents are addressed by their number (`/api/incidents/42` is `INC-0042`). Numbers are sequential per organization with no gaps.
|
||||
Addressed by number (`/api/incidents/42` is `INC-0042`). Numbers are sequential per organization with no gaps.
|
||||
|
||||
**Visibility:** supervisors and above see every incident. Reporters see incidents they reported plus any where they're assigned a corrective action. Anything else answers 404.
|
||||
|
||||
| Method | Path | Who | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| GET | `/api/incidents` | signed in | Paginated `{ items, page, pageSize, total }`, newest first. Reporters see only their own reports |
|
||||
| POST | `/api/incidents` | signed in | Create a report (below). 201 |
|
||||
| GET | `/api/incidents/:number` | signed in | 404 if it doesn't exist or isn't visible to you |
|
||||
| PATCH | `/api/incidents/:number` | supervisor | Any create field plus `status`. Closing sets `closedAt`; reopening clears it. Moving to another site clears the area unless one is given |
|
||||
| GET | `/api/incidents` | signed in | `{ items, page, pageSize, total }`, newest first |
|
||||
| GET | `/api/incidents/export` | signed in | CSV of the same filters (no paging, up to 10,000 rows) |
|
||||
| POST | `/api/incidents` | signed in | Create a report (below). High/critical reports email safety managers and admins |
|
||||
| GET | `/api/incidents/:number` | signed in | Full detail: investigation, root causes, actions, attachments, the `transitions` you may perform (with `blockedReason` when not yet possible) and `can` flags |
|
||||
| PATCH | `/api/incidents/:number` | supervisor | Any create field except `anonymous`. Status changes use transitions |
|
||||
| POST | `/api/incidents/:number/transitions` | see below | `{ to, note? }`. The note is also posted as a comment |
|
||||
| GET/POST | `/api/incidents/:number/comments` | can see it | POST `{ body }` |
|
||||
| GET | `/api/incidents/:number/timeline` | can see it | Audit entries `{ id, at, actor, action, changes }` |
|
||||
| PUT | `/api/incidents/:number/investigation` | supervisor | `{ leadId?, method?, summary?, completed? }`. Creates the investigation if needed |
|
||||
| POST | `/api/incidents/:number/root-causes` | supervisor | `{ category, description, whys? }` |
|
||||
| POST | `/api/incidents/:number/actions` | supervisor | `{ title, description?, kind?, assigneeId?, dueDate?, rootCauseId? }`. Not on closed incidents. Emails the assignee |
|
||||
| POST | `/api/incidents/:number/attachments` | can see it | Multipart, field `file`. Photos (JPEG, PNG, WebP, GIF, HEIC) and PDFs only, detected from content; `MAX_UPLOAD_MB` each (default 10), 20 per incident |
|
||||
|
||||
**List query parameters:** `page` (default 1), `pageSize` (1–100, default 25), `q` (searches title, description and location; `INC-0042` or `42` matches the number), `type`, `severity`, `status`, `siteId`.
|
||||
**List and export filters:** `page`, `pageSize` (1–100, default 25), `q` (title, description, location; `INC-0042` or `42` matches the number), `type`, `severity`, `status`, `siteId`.
|
||||
|
||||
**Create body:**
|
||||
|
||||
@@ -70,8 +87,45 @@ Incidents are addressed by their number (`/api/incidents/42` is `INC-0042`). Num
|
||||
|
||||
- `type`: `near_miss`, `hazard`, `injury`, `illness`, `property_damage`, `environmental`
|
||||
- `severity`: `low`, `medium`, `high`, `critical`
|
||||
- `status`: `reported`, `triage`, `investigation`, `actions`, `verification`, `closed`
|
||||
- `occurredAt` must include a time-zone offset and can't be in the future.
|
||||
- Anonymous reports don't record the reporter at all, so they won't appear in the reporter's own list.
|
||||
- `occurredAt` needs a time-zone offset and can't be in the future. Anonymous reports don't record the reporter at all.
|
||||
|
||||
Every create and update is recorded in the audit log (`audit_log` table) with who made it and what changed.
|
||||
### Workflow
|
||||
|
||||
`reported` → `triage` → `investigation` → `actions` → `verification` → `closed`. The rules live in [`shared/workflow.ts`](../shared/workflow.ts) and are enforced by the API.
|
||||
|
||||
| From | To | Who | Condition |
|
||||
| --- | --- | --- | --- |
|
||||
| reported | triage | supervisor | |
|
||||
| triage | investigation | supervisor | |
|
||||
| triage | actions | supervisor | |
|
||||
| triage | closed | supervisor | Low or medium severity only |
|
||||
| investigation | actions | supervisor | At least one root cause |
|
||||
| actions | investigation | supervisor | |
|
||||
| actions | verification | supervisor | At least one action, none open or in progress |
|
||||
| verification | actions | safety_manager | |
|
||||
| verification | closed | safety_manager | Every (non-cancelled) action verified |
|
||||
| closed | investigation | safety_manager | Reopen |
|
||||
|
||||
## Root causes, actions, attachments
|
||||
|
||||
| Method | Path | Who | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| PATCH | `/api/root-causes/:id` | supervisor | `{ category?, description?, whys? }` |
|
||||
| DELETE | `/api/root-causes/:id` | supervisor | |
|
||||
| GET | `/api/actions` | signed in | `?scope=mine\|all` (`all` needs supervisor) and `?filter=open\|overdue\|done\|all`. Sorted by due date |
|
||||
| PATCH | `/api/actions/:id` | see notes | Supervisors: any field and status except `verified`. Safety managers: also `verified`. Assignees: only `status` among `open`, `in_progress`, `done` |
|
||||
| GET | `/api/attachments/:id` | can see the incident | The file, with a sandboxing Content-Security-Policy |
|
||||
| DELETE | `/api/attachments/:id` | uploader or supervisor | |
|
||||
|
||||
Each action has `overdue: true` when it's open or in progress and its due date has passed (server time zone, `TZ`). Assignees get one email digest per day listing their actions due tomorrow or overdue.
|
||||
|
||||
## Stats
|
||||
|
||||
| Method | Path | Who | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| GET | `/api/stats/overview` | signed in | Open incidents, actions due/overdue, average close time, status counts, weekly trend (12 weeks), incidents by site this year |
|
||||
| GET | `/api/stats/analytics` | signed in | `?days=` (7–730, default 90): counts by type and severity, top root-cause category, on-time action rate, monthly completions |
|
||||
|
||||
Stats only count what the signed-in user can see.
|
||||
|
||||
Every create and update on an incident, its investigation, actions and files is recorded in the audit log with who made it and what changed.
|
||||
+12
-14
@@ -15,6 +15,8 @@ This is a living document. Update it as decisions are made or scope changes.
|
||||
| Auth | Email + password (scrypt), server-side sessions in an http-only cookie, four roles: reporter < supervisor < safety manager < admin | Decided |
|
||||
| Deployment | Self-hosted, single `docker compose up` (app + Postgres); file storage added with photo upload | Decided |
|
||||
| Compliance scope | US OSHA first; data model kept extensible for other regions (e.g. RIDDOR) | Proposed |
|
||||
| Incident PDFs | Browser print-to-PDF from a print stylesheet; server-side PDF deferred (see TODO) | Decided |
|
||||
| Email | SMTP via `SMTP_URL`; without it, logged in development and dropped in production. Delivery setup deferred (see TODO) | Decided |
|
||||
| CI | Gitea Actions (`.gitea/workflows`): lint, typecheck, unit tests, build, e2e | Decided |
|
||||
| Product name | Open SafetyLog EHS (replaced placeholder "IA Tracker" and the prototype's "Safesite" branding) | Decided |
|
||||
|
||||
@@ -46,15 +48,16 @@ This is a living document. Update it as decisions are made or scope changes.
|
||||
|
||||
## Phase 2: Core workflow (MVP)
|
||||
|
||||
- Connect the web app to the API: sign-in and first-run setup screens, incident list and report form using real data
|
||||
- Photo upload (attachments)
|
||||
- Magic-link sign-in and password reset by email
|
||||
- Incident detail page: timeline, investigation, root-cause tools (5 Whys, fishbone), corrective actions (CAPA)
|
||||
- Status workflow: Reported → Triage → Investigation → Corrective actions → Verification → Closed, with role-based transitions
|
||||
- "My actions" list and overdue tracking
|
||||
- Email notifications: assignment, due dates, high-severity alerts
|
||||
- CSV and PDF export
|
||||
- Dashboard metrics computed from real data
|
||||
- [x] Web app connected to the API: sign-in, first-run setup, every page on live data (demo data removed from the frontend)
|
||||
- [x] Photo and PDF upload, checked by file content (not name), size-limited, served with a sandboxing security policy
|
||||
- [x] Magic-link sign-in, password reset by email, email invitations for new users (admins can also set passwords when there's no email)
|
||||
- [x] Incident detail page: details editing, activity log (comments + audit trail), investigation with 5 Whys and fishbone (6M) views, corrective actions (CAPA)
|
||||
- [x] Status workflow Reported → Triage → Investigation → Corrective actions → Verification → Closed, with role rules and guards (e.g. high severity can't skip investigation; closing needs every action verified), shared between API and UI
|
||||
- [x] "My actions" and "All actions" lists with overdue tracking; assignees can update progress on their own actions
|
||||
- [x] Email notifications: high/critical alerts to safety managers, action assignment, daily due/overdue digest
|
||||
- [x] CSV export of the filtered register (protected against spreadsheet formula injection); print-friendly incident page for PDF via the browser
|
||||
- [x] Overview dashboard and Analytics computed from real data; Locations and Settings (people, roles, passwords) pages
|
||||
- [x] End-to-end tests run against the real server: full three-role lifecycle, sign-in, visibility rules, mobile reporting
|
||||
|
||||
## Phase 3: Shop-floor reporting
|
||||
|
||||
@@ -85,8 +88,3 @@ This is a living document. Update it as decisions are made or scope changes.
|
||||
|
||||
Inspections and audit checklists, job safety analysis (JSA), training and certification records, management of change, SDS links, public API and webhooks.
|
||||
|
||||
## Known prototype gaps (to be resolved in Phase 2)
|
||||
|
||||
- The web app still uses in-memory demo data; the API exists but isn't connected yet
|
||||
- Dashboard, analytics and location figures are hardcoded
|
||||
- Export, Add location, Settings, Resources, search, notifications and workspace switcher are non-functional
|
||||
@@ -14,6 +14,15 @@ Open items that aren't tied to a single roadmap phase. Tick them off (or delete
|
||||
- [ ] **Docker build in CI.** Build the image in CI once a runner with Docker is available.
|
||||
- [ ] **Shared login rate limit.** The login limiter is in memory, so it resets on restart and isn't shared between processes. Fine for one container; move it to the database before running several app instances.
|
||||
|
||||
## Phase 2 follow-ups
|
||||
|
||||
- [ ] **Server-side PDF (later).** Decided 2026-10-06 to keep browser-generated PDFs for now: the "Print / PDF" button and print stylesheet are simpler and add no dependencies. Revisit if PDFs need to be produced automatically, e.g. attached to emails or archived on close.
|
||||
- [ ] **Email delivery (later).** Decided 2026-10-06 that the current behavior is fine for now: without `SMTP_URL`, emails print to the server log in development and are dropped (with a startup warning) in production. Later: configure SMTP for real deployments, add a "send test email" button for admins, and consider showing admins a warning in the app when email isn't set up.
|
||||
- [ ] **Notification preferences.** Everyone gets every email that applies to them. Add per-user opt-outs, and an alert-recipients setting per site.
|
||||
- [ ] **Edit corrective actions in the UI.** The API supports changing an action's title, assignee and due date; the incident page only offers status changes and adding new actions.
|
||||
- [ ] **HEIC thumbnails.** iPhone HEIC photos are stored and downloadable but browsers can't show them inline; convert to JPEG on upload.
|
||||
- [ ] **Session check console noise.** Signed-out visitors see one 401 in the browser console from the session check; harmless, but a `200 { user: null }` endpoint would be quieter.
|
||||
|
||||
## Done
|
||||
|
||||
- [x] Docker verified 2026-10-06 (Docker 29.8, Compose v5.5): image builds (~315 MB), `docker compose up` with PostgreSQL 18 passes setup → sign-in → report smoke test; data and sessions survive `down`/`up`; clean shutdown; healthcheck healthy; runs as non-root; standalone PGlite container works; `Secure` cookie with an `https://` PUBLIC_URL; web app loads with no errors.
|
||||
|
||||
Reference in new issue
Block a user