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

- 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:
jayjennings committed 2026-10-06 10:51:18 -07:00
1 parent a382fa904a
commit c69eccb5c1
47 files changed
+6297 -45

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
# API reference
JSON over HTTP under `/api`. Authentication uses a session cookie (`safetylog_session`, http-only) set by setup, sign-in and password change.
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 }]`.
Roles, from least to most privileged: `reporter` < `supervisor` < `safety_manager` < `admin`. A role can do everything the roles below it can.
## Setup and authentication
| Method | Path | Who | Notes |
| --- | --- | --- | --- |
| GET | `/api/health` | anyone | `{ ok: true }` |
| 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/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.
## 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 |
## Sites and areas
| Method | Path | Who | Notes |
| --- | --- | --- | --- |
| 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 |
| 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.
| 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 |
**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`.
**Create body:**
```json
{
"title": "Forklift reversed into pallet stack",
"type": "near_miss",
"severity": "high",
"occurredAt": "2026-10-06T08:15:00-05:00",
"siteId": "…",
"areaId": "…",
"locationDetail": "Loading bay C",
"description": "…",
"immediateAction": "Area cordoned off",
"anonymous": false
}
```
- `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.
Every create and update is recorded in the audit log (`audit_log` table) with who made it and what changed.