- Added a new `api.md` file detailing the TrackGo HTTP API, including agent rules, authentication mechanisms, and global HTTP contracts.
- Established a new RBAC (Role-Based Access Control) framework in `web-rbac.mdc` to ensure all product modules in `apps/web` are gated by permissions from `GET /auth/me`.
- Updated security and web module architecture rules to incorporate RBAC requirements, ensuring consistent application of permissions across modules.
This commit enhances the project's API clarity and security by providing a structured approach to user permissions and interactions.
description: RBAC is required for every apps/web product module (menu, moduleKey, action flags)
globs: apps/web/src/apps/main/**/*.{ts,tsx}
alwaysApply: false
---
# Web module RBAC
Every authenticated product module in `apps/web` must be gated by `GET /auth/me` permissions. Copy Privileges (`system/privileges`) — do not invent a second RBAC path.
Catalog keys live in `api.md` §4 (`PRIVILEGES`, `CONFIGURATION.BRANCH`, `SALES.ORDER`, …). `isSuperadmin` bypasses the matrix (adapter returns `defaultPrivileges`).
## Required wiring (do all four)
1. **`moduleKey`** on `ModuleConfigEntity` equals the catalog `code` (e.g. `CONFIGURATION.BRANCH`).
2. **Menu leaf** in `layouts/data/menu.data.ts` sets the same `moduleKey`. `filterMenuByViewPrivilege` hides the item when `ALLOW_VIEW` is false.
3. **Routes** wrap in `EnterpriseModuleProvider` so missing `ALLOW_VIEW` shows forbidden (no all-true flash).
4. **Do not** re-check create/edit/delete in page JSX. Foundations already hide actions from `PrivilegeEntity`.
```ts
// BAD — custom hide/show, or menu without moduleKey
if (!user.permissions.BRANCHES?.create) return null;
Static contract for a frontend agent or UI. Source of truth is this backend’s controllers and DTOs. Live OpenAPI (when the server is running and Swagger is enabled): `GET /docs` (UI) and `GET /docs-json`.
Base URL: `http://localhost:{PORT}` (default **3000**). There is **no** global path prefix. JSON keys are **camelCase**. IDs are **UUID v4**.
---
## 1. Agent rules (read first)
1. Send `Authorization: Bearer <accessToken>` on every route except `GET /` and `POST /auth/register|login|refresh|revoke`.
2. List endpoints return `{ data, meta }`. Detail, create, update, status, import, and bulk return the **bare** DTO or a small object. `DELETE` is **204** with an empty body (except nested plan destinations, which return the plan).
3. Request dates as ISO datetime or `YYYY-MM-DD`. Response timestamps are **unix milliseconds** (`number`).
4. Phones are E.164 (`+6281234567890`). Money, quantities, and prices are **decimal strings** (for example `"12500.0000"`), never floats.
5. Never send `status` on `PATCH /:id`. Use `PATCH /:id/status` or `POST /bulk-status`.
6. Unknown JSON fields are rejected (`400`). Do not send snake_case aliases.
7. Gate UI with `GET /auth/me` → `isSuperadmin` or `permissions[KEY][action]`. Superadmin bypasses the matrix.
8. Cycles and plans are purpose-scoped (`sales` | `logistics`) and use **different privilege keys**. Pass `purpose` on writes (body) and preferably on lists (query).
9. Cycles and plans `DELETE` / `bulk-delete`**archive** (`status: "archived"`); they do not hard-delete.
10. Default new-record status is `draft` when omitted.
New users have **no privilege** (`privilegeId` null). They cannot call protected CRUD until an admin assigns a privilege (`PATCH /users/:id/privilege`) or they are promoted to superadmin in the database.
#### `POST /auth/login` — public — `200`
Same body and response as register. Invalid credentials → `401`.
#### `POST /auth/refresh` — public — `200`
```json
{"refreshToken":"<opaque>"}
```
`refreshToken` min length 32. Invalid → `401`.
#### `POST /auth/revoke` — public — `204`
Same body. Idempotent: already-invalid tokens still return 204.
- **In:** ISO datetime (`2026-08-24T10:00:00+07:00`) or calendar `YYYY-MM-DD`. Naive ISO (no `Z`/offset) is interpreted in `DEFAULT_TIMEZONE` (default `GMT+7`).
- **Out:** unix **milliseconds** UTC for `date`, `createdAt`, `updatedAt`, `cycleStartDate`, plan `date`.
- Display timezone is GMT+7 unless the backend env says otherwise. Persist/compare using the numeric ms, not a formatted string.
`purpose` is read from **body** (writes) or **query** (lists). If omitted, the user may proceed if they have the action on **either** purpose; list results are filtered to purposes they can view. Superadmin bypasses.
Sales plans may attach `invoiceIds` only. Logistics plans may attach `packingSlipIds` only.
---
## 5. Standard CRUD (most resources)
Unless a section says otherwise, each resource below implements:
**List items omit**`products` / `images` / payment `invoices`. **GET :id, POST, PATCH, PATCH status** include them with generated `id` on each nested row.
**Create fields are optional in JSON.** If `salesOrderId` is set, missing `date` / `customerId` / `address` / `products` are copied from the order. Service still requires a resolved date, customer, and address.
`cycleStartDate` is unix ms for **start of that calendar day** in `DEFAULT_TIMEZONE`. Plan generate rejects ranges that start before this date.
---
## 22. Cycles
Privilege: `RequireFieldPrivilege('cycle', action)` → `SALES.CYCLE` or `LOGISTICS.CYCLE`.
No hard delete: `DELETE` / `bulk-delete` archive. **Has CSV import.** Standard list/detail/create/update/status otherwise.
**Create:**
```json
{
"employeeId":"uuid",
"purpose":"sales",
"cycleNumber":1,
"weekdays":{
"monday":{
"startBranchId":"uuid",
"endBranchId":"uuid",
"customerIds":["uuid"]
}
},
"status":"draft"
}
```
-`purpose`: `sales` | `logistics`
-`cycleNumber`: integer ≥ 1
-`weekdays`: object keyed by `monday`…`sunday`. **Omitted days are days off.** Any present day must include `startBranchId`, `endBranchId`, and a **non-empty**`customerIds`. Incomplete day → `400 Weekday must be complete`.
- Unique per `(employeeId, purpose, cycleNumber)` → `409 Cycle already exists for this employee`
- All referenced branches/customers must have usable lat/lng or route build fails (`400 Weekday route is incomplete`)
**Response `weekdays` is an array**, not an object:
```json
{
"id":"uuid",
"employeeId":"uuid",
"purpose":"sales",
"cycleNumber":1,
"weekdays":[
{
"id":"uuid",
"weekday":"monday",
"startBranchId":"uuid",
"endBranchId":"uuid",
"routeGeometry":{
"type":"LineString",
"coordinates":[[106.8456,-6.2088],[107.0,-6.3]]
},
"destinations":[
{"id":"uuid","customerId":"uuid","sortOrder":0}
]
}
],
"status":"draft",
"createdAt":1710000000000,
"updatedAt":1710000000000,
"createdBy":"uuid",
"updatedBy":"uuid"
}
```
`routeGeometry.coordinates` are GeoJSON `[longitude, latitude]` pairs.
List filters: `employeeId`, `purpose`, `cycleNumber`, `status`, `search`. Without `purpose`, results are limited to purposes the caller can view.
CSV required: `employeeCode`, `purpose`, `cycleNumber`, `weekday`, `customerCodes`, `startBranchCode`, `endBranchCode`. Multiple rows with the same employee/purpose/cycleNumber merge weekdays.
---
## 23. Plans
Privilege: `RequireFieldPrivilege('plan', action)` → `SALES.PLAN` or `LOGISTICS.PLAN`.
**No CSV import.** Delete archives. Extra routes: generate, add/remove destinations.
| Method | Path | Action | Status | Body / response |
Response `date` is unix ms. Includes `routeGeometry`, `destinations`, `invoiceIds`, `packingSlipIds`.
### Generate
```json
{
"employeeId":"uuid",
"purpose":"sales",
"from":"2026-01-12",
"to":"2026-01-25"
}
```
Materializes **active** plans from the employee’s cycles over `[from, to]` (inclusive calendar days).
Requires settings (`GET /settings`). Errors:
-`404 Settings not configured`
-`400 Invalid date range` (`to` before `from`)
-`400 Date is before the cycle start date`
-`400 User has no sales cycle` / `User has no logistics cycle`
`skipped` counts days that already have a plan or have no matching weekday template.
Generated plans are created with status **`active`**.
### Destinations
`POST /plans/:id/destinations`:
-`customerId` required
-`afterDestinationId` optional: insert after that stop; omit to append
- Duplicate customer → `409 Customer is already on this plan`
- Unknown `afterDestinationId` → `400 Destination is not on this plan`
`DELETE …/destinations/:destinationId`:
- Last remaining stop → `400 A live plan must keep at least one destination`
- Unknown id → `404 Destination not found`
Both return the full plan (route geometry is recomputed).
List filters: `employeeId`, `purpose`, `date` (`YYYY-MM-DD`), `status`, `search`.
---
## 24. CSV import
`POST /{resource}/import` — `multipart/form-data`, field **`file`**.
- Max 1_048_576 bytes
- Must be CSV mime or filename ending `.csv`
- Fail the whole batch on row errors
- Optional `status` column; omitted → `draft`
-`createdBy` / `updatedBy` = current user
- Plans have **no** import
---
## 25. Suggested frontend flows
### Session
1.`POST /auth/login` → store both tokens.
2. Attach access token to every request.
3. On `401`, `POST /auth/refresh`; if that fails, logout.
4. Logout: `POST /auth/revoke` then drop tokens.
5. After login, `GET /auth/me` and cache `permissions` / `isSuperadmin` for nav and buttons.
### Configuration screens
Standard list + drawer/form. Status chip uses `/status` and `/bulk-status`. Import uses the CSV headers in each section.
### Sales documents
List without lines. Open detail for `products` / `images`. Creating an order from a request: `POST /sales-orders` with `salesRequestId` and any overrides. Packing slip / invoice can be seeded from parent IDs.
### Field (cycles / plans)
1. Ensure `PATCH /settings` has a `cycleStartDate`.
2. Create cycles per employee + purpose (`weekdays` object in, array out).
3.`POST /plans/generate` for a date range, or `POST /plans` for a one-off day.
4. Edit stops with destination add/remove; do not send `status` on plan PATCH.
5. Scope lists with `?purpose=sales` or `logistics` so privilege and filters match the screen.
### Privilege editor
1.`GET /privilege-keys?limit=200` for the matrix axes.
2.`GET /privileges/:id` for cells (`details`).
3.`PATCH /privileges/:id` with the full `details` array.
Extras: customer contacts, `POST /plans/generate`, plan destinations.
Live schema: `GET /docs-json` (non-production, or `SWAGGER_ENABLED=true`).
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.