- 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.
4.0 KiB
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. 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:
{
"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,environmentalseverity:low,medium,high,criticalstatus:reported,triage,investigation,actions,verification,closedoccurredAtmust 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.