Admin & Messaging API
RBAC administration, audit, per-user UI state, demo data, and the three realtime features (announcements, notifications, chat) plus the STOMP WebSocket they ride on. Conventions: conventions.md. RBAC concepts and data model: ../security/authentication.md.
Tenants — /api/admin/tenants
The platform operator's registry of companies. Gated on the Platform
permission category, which no tenant role holds — a tenant's own System
Administrator, granted every permission in their own world, is granted nothing
here.
| Method | Path | Permission | Purpose |
|---|---|---|---|
| GET | /api/admin/tenants | platform.tenants.view | List (paged, with the usual grid filter params) |
| GET | /api/admin/tenants/{id} | platform.tenants.view | One tenant |
| POST | /api/admin/tenants | platform.tenants.manage | Provision a company and its first administrator |
| PUT | /api/admin/tenants/{id} | platform.tenants.manage | Update name / timezone / admin email (the slug is fixed) |
| PATCH | /api/admin/tenants/{id}/suspend | platform.tenants.manage | Lock the company out |
| PATCH | /api/admin/tenants/{id}/reactivate | platform.tenants.manage | Let it back in |
| POST | /api/admin/tenants/{id}/repair | platform.tenants.manage | Finish a provisioning run that did not complete |
POST returns 201 with the tenant, the administrator's username, and their
temporary password. Only a hash is stored, so this response is the only
readable copy the API ever produces. The same password is also emailed to
adminEmail (backend/email.md), but on a best-effort
basis — a failed send does not fail the 201, which is why the operator still
gets a copy to pass on. That account can do nothing until the password is
changed (see auth). A duplicate slug, username or email is a 409.
Suspending tenant 1 is a 409: it owns every pre-multi-tenancy row, so
suspending it would lock the deployment out of itself.
Provisioning seeds the company's roles (cloned from the blueprints), payroll
settings, company profile, CRM settings and picklists, leave types, deduction
types, de minimis types, a starting department and positions, and two years of
Philippine holidays. The tenant is created suspended and activated only once
that finishes, so a failure leaves it visibly incomplete rather than
half-working; repair re-runs the same steps, each of which skips what already
exists.
Users — /api/users
| Method | Path | Permission | Purpose |
|---|---|---|---|
| GET | /api/users · /{id} | system.admin.users.view | List / one user (scoped to the caller's tenant) |
| POST | /api/users | system.admin.users.create | Create (links a user to an employee + roles) |
| PUT | /api/users/{id} | system.admin.users.edit | Update |
| DELETE | /api/users/{id} | system.admin.users.delete | Delete |
Roles — /api/roles and Permissions — /api/permissions
RoleController.java, PermissionController.java
| Method | Path | Permission | Purpose |
|---|---|---|---|
| GET | /api/roles · /{id} | system.admin.roles.view | List / one role |
| POST | /api/roles | system.admin.roles.create | Create |
| PUT | /api/roles/{id} | system.admin.roles.edit | Update |
| DELETE | /api/roles/{id} | system.admin.roles.delete | Delete |
| PUT | /api/roles/{id}/permissions | system.admin.role.permissions.manage | Replace a role's permission set |
| GET | /api/permissions · /{id} | system.admin.permissions.view | List / one permission |
| POST | /api/permissions | system.admin.permissions.create | Create |
| PUT | /api/permissions/{id} | system.admin.permissions.edit | Update |
| DELETE | /api/permissions/{id} | system.admin.permissions.delete | Delete |
Permission names are what @PreAuthorize checks — creating a permission row
here does nothing until a controller references it
(conventions.md).
Audit logs — /api/audit-logs
AuditLogController.java:
GET (paged/filtered), permission system.admin.logs.view. Rows are written by
the backend for security-relevant actions (logins, refresh-reuse detection,
payroll cancellations…). New entries are also broadcast live to WebSocket
topic /topic/audit-logs, which the admin UI tails.
Saved views — /api/saved-views
SavedViewController.java — named grid configurations for the enterprise grid (../frontend/ag-grid.md). Personal data: every endpoint is scoped to the caller's own userId, so authentication alone suffices — no permission gate.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/saved-views?entityKey= | Views the caller saved for one grid |
| POST | /api/saved-views | Save a view (name + serialized grid state) |
| DELETE | /api/saved-views/{id} | Delete own view |
User nav preference — /api/user-nav-preference
UserNavPreferenceController.java — per-user sidebar personalization; authenticated only, same reasoning as saved views. GET (204 when unset), PUT (save), DELETE (reset to default).
Demo data — /api/system-admin/demo-data
DemoDataController.java
— all three gated by system.admin.demo.data.manage; drives the seeder
described in ../architecture.md §6:
| Method | Path | Purpose |
|---|---|---|
| GET | /api/system-admin/demo-data/status | Row counts / whether demo data exists |
| POST | /api/system-admin/demo-data/generate | Generate transactional demo data (timesheets, leave, payroll runs…) |
| POST | /api/system-admin/demo-data/reset | Wipe and regenerate |
Announcements — /api/announcements
| Method | Path | Permission | Purpose |
|---|---|---|---|
| GET | /api/announcements · /{id} | any authenticated user | Paged list / one announcement (everyone reads them) |
POST · PUT /{id} · DELETE /{id} | /api/announcements... | hr.announcements.manage or system.admin.announcements.manage | Author / edit / remove |
| PATCH | /api/announcements/{id}/archive · /restore | same two | Soft-delete / restore |
Notifications — /api/notifications
NotificationController.java
— all three require employee.notifications.view:
| Method | Path | Purpose |
|---|---|---|
| GET | /api/notifications/me | Own notifications |
| PATCH | /api/notifications/{id}/read | Mark one read |
| PATCH | /api/notifications/me/read-all | Mark all read |
New notifications are also pushed over the WebSocket to the per-user queue
/user/queue/notifications — the bell icon updates without polling.
Chat — /api/conversations + the STOMP WebSocket
ConversationController.java
— all endpoints require employee.messages.view:
| Method | Path | Purpose |
|---|---|---|
| GET | /api/conversations/me | Own conversation list |
| GET | /api/conversations/{id} | One conversation (participants only) |
| GET | /api/conversations/{id}/messages | Message history (paged) |
| GET | /api/conversations/{id}/messages/cursor?before=&limit= | Cursor-paged history (infinite scroll) |
| POST | /api/conversations/messages | Send: {"recipientEmployeeId": 10007, "messageContent": "..."} |
Sending is HTTP; delivery is WebSocket — the recipient (and the sender's other tabs) receive the message on their user queue.
The WebSocket contract
Verified in WebSocketConfig.java and WebSocketAuthChannelInterceptor.java; overview in ../backend/scheduling-and-websockets.md.
- Handshake:
/ws(SockJS). In dev-compose that's proxied athttp://localhost:5173/wsand direct athttp://localhost:8081/ws. - Authentication: the STOMP CONNECT frame must carry
Authorization: Bearer <accessToken>as a native header — the interceptor validates the JWT and rejects the connection otherwise. (The HTTP handshake itself is not where auth happens.) - Prefixes: subscribe on
/topic/**(broadcast) and/queue/**(per-user, via the/userprefix); send application frames to/app/**.
| Destination | Direction | What flows |
|---|---|---|
/user/queue/messages | subscribe | New ChatMessageDtos for you (as recipient or echo as sender) |
/user/queue/conversations | subscribe | Conversation-level updates (unread badges) |
/user/queue/notifications | subscribe | New notification DTOs |
/topic/audit-logs | subscribe | Live audit-log rows (admin UI) |
/app/conversation/{id}/typing | send | Typing indicator (TypingController.java) |
/topic/conversation/{id}/typing | subscribe | Typing indicator fan-out to the conversation |
Minimal client (matches the frontend's usage)
The real implementations are
frontend/src/hooks/useMessagingWebSocket.ts,
useNotificationsWebSocket.ts, and useAuditLogWebSocket.ts (same directory):
import { Client } from '@stomp/stompjs';
import SockJS from 'sockjs-client';
const client = new Client({
webSocketFactory: () => new SockJS(`${API_BASE_URL}/ws`),
connectHeaders: { Authorization: `Bearer ${accessToken}` },
reconnectDelay: 5000,
onConnect: () => {
client.subscribe('/user/queue/messages', (frame) => {
const msg = JSON.parse(frame.body);
// append to the conversation…
});
client.subscribe('/user/queue/notifications', (frame) => { /* bell badge */ });
},
});
client.activate();
// publishing (e.g. typing indicator):
client.publish({
destination: `/app/conversation/${conversationId}/typing`,
body: JSON.stringify({ typing: true }),
});
Note the token in connectHeaders is captured at connect time; after a token
refresh the next reconnect uses whatever the hook reads from the store.