40227-vm/docs/full-integration-refactor-plan.md
2026-06-10 18:59:43 +02:00

623 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Full Integration Refactor Plan
## Purpose
This document is the active cross-application backlog for integrating `frontend/`, `backend/`, and the PostgreSQL database.
It must track only unfinished integration work and hard implementation rules. Completed migration history belongs in the feature-specific docs under `backend/docs/`, `frontend/docs/`, and supporting audit docs.
When an item is completed, remove it from this plan and update the owning feature documentation instead of adding a completed checklist entry here.
## Current Application Shape
- `frontend/` is the only product frontend.
- `backend/` is the source of truth for auth, tenant ownership, roles, permissions, users, staff profiles, campuses, persisted workflows, content catalogs, email, files, and database access.
- `ref-frontend/` is a temporary generated reference. It is not runtime code and must be deleted after all needed endpoint-contract reference value is exhausted.
## Non-Negotiable Rules
- No frontend persisted workflow may use mock, sample, seed, or fallback records.
- No frontend runtime code may import backend seed files.
- Frontend runtime constants may contain only UI config, route/module metadata, query keys, timing values, style tokens, static UI labels, and intentionally static product copy.
- Backend seeds are for database seeding only.
- Frontend seed data is allowed only in tests or `frontend/src/test-seeds/`.
- Backend owns tenant scoping and permissions. Frontend route hiding is UX only.
- All frontend backend calls go through typed modules in `frontend/src/shared/api/`.
- New frontend workflows must follow `View -> Business Logic -> API/Data Access -> Backend`.
- View components must not import `frontend/src/shared/api/`.
- Business logic must not import view components.
- API modules must not import business or view code.
- All imports use the `@` alias.
- No `any`, unsafe casts, disabled TypeScript rules, disabled ESLint rules, compatibility bypasses, silent failures, or legacy re-export surfaces.
- Frontend auth must use backend-owned HttpOnly cookies only. Access and refresh tokens must never be stored in frontend browser storage.
- Secrets live only in backend `.env` or deployment environment variables. Frontend env values must be public browser-safe values only.
- New backend modules must include migration/model/service/route, tenant and role enforcement, docs, and focused verification.
- Every changed workflow must update the relevant docs before it is considered complete.
## Current Verification Baseline
Frontend current baseline:
- `npm run typecheck` passes.
- `npm run lint` passes.
- `npm run test` passes with 51 files and 198 tests.
- `npm run build` passes.
- `npm run test:e2e` passes with 4 backend-free Playwright smoke tests.
- `npm run test:e2e:content` exists for backend-seeded content catalog integration tests. It requires backend migrations, backend seeders, and a running backend server.
Backend current baseline:
- Runtime code is 100% TypeScript + native ESM; the JS->TS / CJS->ESM migration is complete.
- `npm run typecheck` (`tsc --noEmit`) passes.
- `npm run lint` (`eslint .`) passes with no broad ignores.
- `npm test` (`node --test` via `tsx`) passes: 2 files, 15 tests (error-handler + import-boundaries).
- `npm run verify` (typecheck + lint + test) is the combined gate and is green.
- `npm run build` (`tsc` + `tsc-alias -f` + email-template asset copy) produces a runnable `dist/`.
- Migrations/seeders run via Umzug (`npm run db:migrate`, `npm run db:seed`); a run against the configured local database is still pending (Workstream 1).
## Active Workstreams
The workstreams are numbered in dependency/execution order and grouped into five phases. Earlier phases unblock later ones; within a phase, items may run in parallel unless a dependency is stated. Renumbered from the previous flat backlog: the former Role Model Decision, Product Onboarding Contract, and Permission-Based Frontend Authorization workstreams are now merged into Workstream 3 (RBAC); the former standalone seed-verification step is merged into Workstream 4.
**Phase 1 — Foundation (everything else depends on a backend that boots, migrates, and serves routes).**
### 1. Backend Migration And Runtime Boot
Status: open.
Problem:
Backend product modules exist, but the configured local database migration/seed run has not been verified as a passing gate in this plan.
Required work:
1. Run `npm run db:migrate` against the configured local database.
2. Run `npm run db:seed` (current seed set; the final RBAC seed content is delivered in Workstream 4, which re-verifies this step).
3. Start backend with the documented env file.
4. Verify public content catalog routes, auth routes, and product module routes respond.
5. Record exact failing migration/seed/runtime errors if any.
Acceptance criteria:
- `npm run db:migrate` passes.
- `npm run db:seed` passes.
- Backend starts without generated default secrets.
- `GET /api/public/content-catalog/:contentType` works for the required seeded content catalog types.
- Any remaining backend runtime blocker is captured as a specific follow-up with file/error references.
**Phase 2 — Identity, Tenancy & Access Core (the security spine: tenant isolation, the role/permission model, the seeded fixtures, and the finalized protected API surface). Workstreams 57 finalize what is public, what exists, and what is permission-gated, so they belong with the access model rather than after it.**
### 2. Tenant Boundary Audit And Tests
Status: open and high priority. Co-designed with Workstream 3: the campus/organization scoping here and the `campusId`-on-`users` model in §3.1 are the same scoping layer.
Problem:
Generated backend code has partial tenant scoping. Multi-tenant correctness cannot rely on frontend filtering.
Required work:
1. Audit all tenant-owned generated and product routes for organization scoping.
2. ~~Resolve inconsistent `organizationsId` versus `organizationId` usage~~ — done: unified on `organizationId` for all models (renamed the `users.organizationsId` column via migration `20260609000000-rename-users-organizationsid-to-organizationid`, updated db/api scoping, the auth DTO, and the frontend `CurrentUser` type). Verify the tenant-scoping `where` now correctly targets each model's `organizationId`.
3. Ensure create/update/delete paths cannot accept another tenant's organization or campus.
4. Ensure list/count/autocomplete endpoints are tenant-scoped.
5. Add backend tests proving cross-tenant records are not visible or mutable.
Acceptance criteria:
- Tenant isolation tests cover users, campuses, staff, students, attendance, messages, documents, and product module tables.
- A non-global user cannot read or mutate another tenant's data.
- Campus-scoped users cannot mutate another campus unless their backend permission explicitly allows it.
### 3. RBAC: Role Hierarchy, Scoped Provisioning, And Guards
Status: open and high priority. Umbrella workstream for the platform's user/role model. **Merged here:** the former Role Model Decision (role-model choice), the former Product Onboarding Contract (provisioning, §3.4), and the former Permission-Based Frontend Authorization (frontend guards, §3.6). Consumes the existing permission engine in `middlewares/check-permissions.ts`.
Decisions recorded (owner-approved):
- Role model: **first-class persisted roles** in the `roles` table, each carrying a `scope`, with preset permission sets and code-enforced relational constraints. The `GENERATED_ROLE_TO_PRODUCT_ROLE` / `STAFF_TYPE_TO_PRODUCT_ROLE` mappings and the 5-value `productRole` are retired.
- Frontend: **permission-based guards plus an authentication guard**; forbidden direct-URL access redirects to the 404 page; menu items the user cannot access are hidden.
#### 3.0 Current state (analysis, verified)
- Roles today (`backend/src/db/seeders/20200430130760-user-roles.ts`, `backend/src/shared/constants/roles.ts`): 7 generated roles (`Super Administrator`, `Administrator`, `Platform Owner`, `Tenant Director`, `Campus Manager`, `Academic Coordinator`, `Finance Officer`) + `Public` + a `User` default-role name referenced in code. `globalAccess: true` is set only on `Super Administrator` and `Administrator`. These map down to 5 `productRole` values (`teacher | para | office | director | superintendent`).
- Authorization engine: `middlewares/check-permissions.ts` grants a request when (1) `currentUser.id === req.params.id || req.body?.id` (self-access), (2) a `custom_permissions` row matches, or (3) the effective role (assigned `app_role`, else cached `Public`) has the `${METHOD}_${ENTITY}` permission. CRUD routers wire `checkCrudPermissions(entity)`. This engine is sound and is kept.
- Permission catalog: CRUD × 27 entities + `READ_API_DOCS` + `CREATE_SEARCH`. **Product modules are not represented** (no permission exists for the FRAME, walkthrough, quizzes, director-dashboard, attendance-fill, ESA/vocational/community pages, classrooms, etc.).
- Multi-tenancy: `organizationId` on most models (unified, per Workstream 2). Campus link for a user is only indirect, via the `staff` row's `campusId`; `staff_type` is `teacher | admin | support`. There is **no `campusId` on `users`** and no first-class concept of director/owner/student/guardian.
- Seeders: **no organization row is seeded at all**, and the seeded campuses (`shared/constants/campuses.ts`, 6 rows) have **no `organizationId`**. `20200430130760` assigns roles by raw SQL: `super_admin@flatlogic.com`→SuperAdmin, `admin@flatlogic.com`→Administrator, `client@hello.com`→PlatformOwner, `john@doe.com`→TenantDirector. **This contradicts the product spec**, which states `admin@flatlogic.com` (`SEED_ADMIN_EMAIL`) is the super admin. No staff profiles are seeded.
- Frontend: `ModuleRouteGuard` + sidebar filter gate purely on the 5-value `productRole` (`shared/constants/appData.ts` `MODULES[].roles`, `shared/constants/moduleRoutes.ts`). `CurrentUser.permissions` exists but is unused. There is **no authentication guard** (an unauthenticated user reaching the shell is not redirected to `/login`), and `canUserRoleAccessModuleRoute` returns `true` for unknown paths (forbidden routes fall through to the catch-all rather than failing at the guard). Only `/login` and `*` (404) are public; there is no signup route.
#### 3.1 Target role hierarchy and scope
Eleven roles across five scopes (guest is the unauthenticated case, not a stored user):
1. `super_admin` — scope `system`. Unlimited across the whole platform.
2. `system_admin` — scope `system`. Unlimited platform-wide **except** creating/deleting `super_admin` or other `system_admin`.
3. `owner` — scope `organization`. Unlimited inside its own company.
4. `superintendent` — scope `organization`. Unlimited inside the company **except** creating/deleting `owner` or other `superintendent`, and **except** deleting the company profile.
5. `director` — scope `campus`. Unlimited inside its assigned campus; creates campus users and classrooms; grants extra per-user permissions.
6. `office_manager` — scope `campus`. Reads all campus pages **except** the director dashboard; takes quizzes; leaves read receipts; may fill the attendance page.
7. `teacher` — scope `campus`. Reads all campus pages except the director dashboard.
8. `support_staff` — scope `campus`. Reads all campus pages except the director dashboard. (Replaces the current `para` value.)
9. `student` — scope `external`. Reads only `/community-partnerships`, `/vocational-opportunities`, `/esa-funding`.
10. `guardian` — scope `external`. Same external pages as `student`.
11. `guest` — unauthenticated. Public routes only (`/login`, `/signup`, error page); any other URL redirects to 404.
Required work:
1. Add a `scope` column to the `roles` model/migration: enum `system | organization | campus | external | guest`. Keep `globalAccess` for the two system roles (it already short-circuits tenant filtering in the CRUD layer).
2. Replace the seeded role set with these 11 (plus keep `Public` as the unauthenticated fallback mapped to `guest`). Write a data migration mapping legacy roles → new roles (`Super Administrator``super_admin`, `Administrator``system_admin`, `Platform Owner``owner`, `Tenant Director``superintendent`, `Campus Manager``director`, `Academic Coordinator``teacher`, `Finance Officer``office_manager`; `para`/`support` HR type → `support_staff`).
3. Retire `GENERATED_ROLE_TO_PRODUCT_ROLE`, `STAFF_TYPE_TO_PRODUCT_ROLE`, and `PRODUCT_ROLE_VALUES`; the role name + scope is now the source of truth. Replace `productRole` in the `/api/auth/me` DTO with `role: { name, scope }` (keep a stable string the frontend can switch on).
4. Add a campus link for campus-scoped users. Recommended: a nullable `campusId` on `users` (authorization scope), independent of the HR `staff` row. `organizationId` remains the tenant key; `campusId` is required for `director`/`office_manager`/`teacher`/`support_staff` and optional for `student`/`guardian`; null for `system`/`organization` scopes. (Same scoping layer as Workstream 2.)
#### 3.2 Permission catalog expansion
The frontend can only gate on permissions the backend actually checks, so every guarded page/action needs a permission name.
1. Keep the CRUD × entity permissions; add `classrooms` (alias of `classes` if the same table, otherwise a new entity) to CRUD.
2. Add product-feature permissions for the module routes and special actions, named in the existing `${VERB}_${THING}` style — e.g. `READ_DIRECTOR_DASHBOARD`, `FILL_ATTENDANCE`, `TAKE_QUIZ`, `ACK_READ_RECEIPT`, `READ_COMMUNITY_PARTNERSHIPS`, `READ_VOCATIONAL_OPPORTUNITIES`, `READ_ESA_FUNDING`, plus one per remaining campus module page.
3. Define the **role → permission preset matrix** (the "predefined permissions per role" from the spec) and seed it into `rolesPermissionsPermissions`. Directors additionally hand out per-user `custom_permissions`.
4. Document the matrix in `backend/docs/auth-profile.md` (or a new `backend/docs/rbac.md`).
#### 3.3 Relational role constraints (the "except …" rules)
The constraints are about *who the target is*, not just *what verb on what entity*, so a flat `${METHOD}_${ENTITY}` permission cannot express them. Implement a policy layer enforced in the **service layer** (authoritative), with a middleware in front for early rejection:
1. On user create/update/delete, compare actor scope+role against the target's role: `system_admin` cannot create/delete `super_admin` or `system_admin`; `superintendent` cannot create/delete `owner` or `superintendent`; campus roles cannot escalate above their scope; a director may only create/manage users within its own `campusId`.
2. On organization delete, require `super_admin`/`system_admin`/`owner` (a `superintendent` is blocked).
3. Cross-scope tenancy: every actor may only act within its `organizationId` (and `campusId` for campus scope) unless `globalAccess`.
4. Fix the self-access bypass security hole: `currentUser.id === req.body?.id` (`middlewares/check-permissions.ts`, the self-access branch) lets any user pass a guard by putting their own id in the request body; restrict self-access to read/update of the caller's own profile only.
#### 3.4 Scoped provisioning / onboarding contract (former Product Onboarding Contract)
Auto-provisioning chain, all backend-owned, each step sending an invitation email with a login link (`services/email/list/invitation.ts` already exists). No-go rules carried over from the former onboarding workstream: do not implement frontend self-registration; do not build profile creation/editing UI ahead of the backend contract; do not treat the generated `signup`/`profile` endpoints as the product contract; no temporary compatibility paths.
1. `super_admin`/`system_admin` create an `owner` user → **auto-create the company (organization)** and link the owner; both start minimal (owner has only email+password; company has only the owner link).
2. `owner`/`superintendent` create superintendents, other org users, and campuses; assign a `director` to a campus.
3. `director` creates campus users (`teacher`, `support_staff`, `office_manager`, `student`, `guardian`) and classrooms, and grants extra `custom_permissions`.
4. Every created user receives a login-link email; on first login they land on their own editable profile.
5. Define which profile fields each role may self-edit vs. which require a higher role.
#### 3.5 Backend guard coverage audit
1. Confirm **every** route mounts authentication + `checkCrudPermissions`/`checkPermissions` (cross-reference Workstream 5 public-route audit so only intended routes are public).
2. Add the §3.3 relational policy to the user, organization, campus, staff, and classroom write paths.
3. Add the product-feature permission checks (§3.2) to the feature routes (FRAME, walkthrough, quizzes, attendance, communications, content-catalog, etc.).
4. Ensure list/count/autocomplete are tenant- and campus-scoped (ties to Workstream 2).
#### 3.6 Frontend guards (former Permission-Based Frontend Authorization)
Context: the backend already authorizes every request by **permission**, not role; the user DTO already carries `permissions: string[]`, but the frontend ignores it and gates on the 5-value `productRole`. The goal is to gate UI affordances (routes/menu items/buttons/request triggers) by **permission** — using the same `${METHOD}_${ENTITY}` names the backend checks — while the backend stays the sole source of truth (frontend gating is UX-only and must never be the only enforcement).
1. **AuthGuard.** Wrap the shell: unauthenticated → redirect to `/login`; only `/login`, `/signup`, and the error page are reachable without a session.
2. **Expose permissions in the auth contract.** Confirm `GET /api/auth/me` and the sign-in response include the resolved effective `permissions: string[]` (role permissions `custom_permissions`); merge `custom_permissions` if not already; document the field in `backend/docs/auth-profile.md`.
3. **Shared permission vocabulary.** Add a typed catalog on the frontend (`frontend/src/shared/auth/permissions.ts`) mirroring backend `${METHOD}_${ENTITY}` names; keep it as `shared/constants`-style UI config; do not import backend code.
4. **Permission selector/hook.** Implement `hasPermission/hasAnyPermission/hasAllPermissions` in `frontend/src/business/auth/` (pure functions over `CurrentUser.permissions`) plus a `usePermissions()` hook.
5. **Gate routes by permission.** Replace `productRole`-based `canUserRoleAccessModuleRoute`/`getAccessibleModules` with permission checks; each `MODULES[]` entry declares the permission(s) it requires. Keep a role concept only where genuinely role-specific (e.g. dashboards), not for resource access.
6. **Forbidden direct-URL access redirects to the 404 page** (change `canUserRoleAccessModuleRoute`'s unknown-path `return true` and the guard's redirect target); inaccessible sidebar items are hidden.
7. **Encode the role-specific page sets.** `office_manager` sees all campus pages except the director dashboard (plus quiz/read-receipt/attendance-fill affordances); `teacher`/`support_staff` see all campus pages except the director dashboard; `student`/`guardian` see only `/community-partnerships`, `/vocational-opportunities`, `/esa-funding`; `director`/`superintendent`/`owner` see their full scope; `guest` sees only public routes.
8. **Gate affordances + handle 403.** Hide/disable create/edit/delete triggers by the matching permission (e.g. hide "Add campus" without `CREATE_CAMPUSES`); surface backend `forbidden` responses through one handler (toast + no crash) so UI/permission drift degrades gracefully.
9. **Roles/permissions admin UI.** Let an admin/director create a role, attach/detach permissions, and assign `custom_permissions` to a user — backed by the existing `roles`/`permissions`/`users` endpoints.
10. **Docs.** Update `frontend/docs/frontend-architecture.md` and `backend/docs/auth-profile.md` to describe the permission-based frontend model and the `${METHOD}_${ENTITY}` contract.
#### 3.7 Tests
1. Backend: relational-constraint tests (`system_admin` cannot delete `super_admin`; `superintendent` cannot delete `owner`/`superintendent` or the company; director scoped to its campus), tenant/campus isolation, and the provisioning chain (owner-create auto-creates company).
2. Frontend: per-role route-guard tests (each role can/cannot open the right pages), sidebar-visibility tests, `hasPermission`/selector unit tests, unauthenticated→`/login`, and forbidden-URL→404.
3. Backend-seeded authenticated e2e using the Workstream 4 fixtures (feeds Workstream 8) proving UI affordances match backend enforcement for at least one CRUD entity.
Acceptance criteria:
- The 11 roles exist as first-class scoped roles; the legacy mapping and 5-value `productRole` are gone.
- Backend is the sole authority: every route enforces authentication + permission + (where relational) the §3.3 policy; removing a frontend check never grants real access (backend still returns 403).
- The "except …" constraints hold in tests for `system_admin`, `superintendent`, and `director`.
- Frontend route/menu/affordance visibility is permission-driven; unauthenticated users are redirected to `/login`; forbidden direct URLs land on 404; guest sees only public routes.
- An admin can create a role, assign permissions, and the change is reflected in both backend enforcement and frontend UI for affected users.
- Owner-creation auto-creates the company; each provisioning step emails a login link.
- Backend and frontend `typecheck`/`lint`/`test` pass; the Workstream 4 seed runs clean and backs an authenticated e2e.
### 4. RBAC Seed Data And Fixtures
Status: open — depends on Workstream 3 (run after the role/permission/model changes land). Implements the spec's seed requirement: a preset company with one campus and staff covering every role, plus one test user per role. Also re-verifies the `npm run db:seed` gate from Workstream 1 against the final seed content.
Required work:
1. **Reconcile the admin identity with the spec.** Make `admin@flatlogic.com` (`SEED_ADMIN_EMAIL`) the **`super_admin`** (the spec's superadmin), not `Administrator`. Add a distinct `system_admin` seed user. Repurpose or delete the leftover template users (`john@doe.com`, `client@hello.com`, `super_admin@flatlogic.com`) so the seed set is exactly the role fixtures below.
2. **Seed one organization (company)** and **one campus** linked to it (give the existing `campuses` seed rows an `organizationId`, or seed a dedicated test campus).
3. **Seed 10 users, one per stored role**`super_admin`, `system_admin`, `owner`, `superintendent`, `director`, `office_manager`, `teacher`, `support_staff`, `student`, `guardian` — each with `app_roleId` set, `organizationId` set (except the two `system` users), and `campusId` set for the campus-scoped and external users as applicable. (`guest` = no user.) Passwords come from `SEED_ADMIN_PASSWORD`/`SEED_USER_PASSWORD` (no plaintext in the repo); test credentials live only in ignored local env or a dedicated test-seed config (per Workstream 8).
4. **Seed staff profiles** for the campus staff roles (`director`, `office_manager`, `teacher`, `support_staff`) so the campus has staff covering every staff role, each linked to the org, the campus, and its user.
5. Make the seed **idempotent** and reversible (clean `down`), and ordered after the role/permission seeder.
6. Re-verify the seeded data: one company, one campus under it, staff with all campus roles, and exactly one loginable user per role. Add a seed/integration check.
Acceptance criteria:
- `npm run db:seed` produces exactly one company, one campus under that company, staff covering every campus role, and one user per role with correct role/org/campus links.
- `admin@flatlogic.com` is the `super_admin` and can log in; each seeded role can log in and lands on the access surface defined in §3.6.
- No template leftover users remain; no plaintext credentials are committed.
### 5. Public Backend Route Audit
Status: open. Feeds Workstream 3 §3.5 (guard coverage): the guard audit can only assert "every non-public route is authenticated" once the public set is fixed.
Problem:
Public routes must be intentionally public. This includes public content catalog routes and any generated/template public integrations.
Required work:
1. List all routes without auth middleware.
2. Mark each as public-by-design or requiring auth.
3. Add auth where required.
4. Document intentionally public routes.
Acceptance criteria:
- Every unauthenticated backend route is documented.
- No tenant-owned data is exposed through accidental public routes.
### 6. API Surface Coverage And Dead-Endpoint Decision
Status: open (analysis complete; decision pending). Belongs with the access core: the permission catalog (§3.2) and guard audit (§3.5) must cover exactly the endpoints that survive this classification.
Problem:
The backend exposes the full Flatlogic-generated CRUD surface for all 39 models plus
template auth/file/search routes, but the product frontend only consumes a small set of
custom feature endpoints. The unused surface is dead code and attack surface; it must be
either pruned or intentionally wired.
Method (how this was established):
- Enumerated every backend route from `backend/src/routes/*` plus its mount prefix in `backend/src/index.ts`.
- Enumerated every frontend HTTP call. All frontend HTTP goes through `frontend/src/shared/api/*` over `httpClient` (`fetch`); there are no stray `fetch`/`axios`/`XMLHttpRequest` calls elsewhere, so the frontend call list is exhaustive.
Findings:
- **Frontend is fully wired:** every `shared/api` method targets a real, mounted backend endpoint. There are **0 orphan/broken frontend calls**.
- **Backend is only partially consumed:** of ~278 endpoints, the frontend uses **~35 (~13%)**; **~243 (~87%) have no frontend consumer.**
- The mismatch is one-directional: the frontend calls nothing extra; the backend carries a large unused layer.
Consumed endpoints (the only wired surface — 35):
- `auth` (4 of 16): `POST /api/auth/signin/local`, `GET /api/auth/me`, `POST /api/auth/refresh`, `POST /api/auth/signout`.
- `campuses` (1): `GET /api/public/campuses` (the authenticated `/api/campuses` CRUD is NOT used).
- `content-catalog` (6): `GET /api/public/content-catalog/:contentType`, `GET /api/content-catalog`, `GET /api/content-catalog/:contentType`, `POST /api/content-catalog`, `PUT /api/content-catalog/:contentType`, `DELETE /api/content-catalog/:contentType`.
- `campus_attendance` (4): `GET /configs`, `PUT /configs/:campusKey`, `GET /summaries`, `PUT /summaries/:campusKey/:date`.
- `communications` (4): `GET /parent-messages`, `POST /parent-messages`, `GET /events`, `POST /events`.
- `frame_entries` (3): `GET /`, `POST /`, `PUT /:id`.
- `personality_quiz_results` (3): `GET /me`, `PUT /me`, `GET /distribution`.
- `safety_quiz_results` (2): `GET /`, `POST /`.
- `staff_attendance` (2): `GET /records`, `GET /summary`.
- `user_progress` (3): `GET /`, `POST /`, `DELETE /by-item`.
- `walkthrough_checkins` (3): `GET /`, `POST /`, `DELETE /:id`.
Unused backend endpoints (no frontend consumer):
1. **Generic CRUD template — 25 route groups × 9 endpoints = 225, none called:**
`academic_years`, `assessments`, `assessment_results`, `attendance_records`,
`attendance_sessions`, `campuses` (the `/api/campuses` CRUD), `classes`,
`class_enrollments`, `class_subjects`, `fee_plans`, `grades`, `guardians`,
`invoices`, `message_recipients`, `messages`, `organizations`, `payments`,
`permissions`, `roles`, `staff`, `students`, `subjects`, `timetable_periods`,
`timetables`, `users`.
Each exposes the identical shape: `POST /`, `POST /bulk-import`, `PUT /:id`,
`DELETE /:id`, `POST /deleteByIds`, `GET /`, `GET /count`, `GET /autocomplete`,
`GET /:id`.
2. **`auth` extras — 12, not called:** `POST /api/auth/signup`, `PUT /api/auth/profile`,
`PUT /api/auth/password-reset`, `PUT /api/auth/password-update`,
`POST /api/auth/send-password-reset-email`,
`POST /api/auth/send-email-address-verification-email`,
`PUT /api/auth/verify-email`, `GET /api/auth/email-configured`,
`GET /api/auth/signin/google` (+ `/callback`), `GET /api/auth/signin/microsoft` (+ `/callback`).
3. **`file` — 2, not called:** `GET /api/file/download`, `POST /api/file/upload/:table/:field`
(the frontend never uploads; `DocumentMutationDto` has no `file`).
4. **`search` — 1, not called:** `GET /api/search`.
**Decision (owner, recorded):** the generic CRUD layer is **WIRE — kept, not
pruned.** These ~24 groups are not dead code; they will be used and integrated
with the frontend later (likely modeled on `ref-frontend`). No generic CRUD group
is to be removed. The `auth`/`file`/`search` extras (items 24 below) remain
individually decision-gated.
Required work:
1. Generic CRUD groups: **WIRE** (decided above) — build the frontend that uses
them (e.g. real `students`/`staff`/`guardians`/`invoices` management) and bring
each group up to the target backend architecture. Do **not** prune them.
2. `auth` extras: keep `signup`/password-reset/verify-email only if onboarding/recovery is
in scope (see Workstream 3 §3.4 provisioning); otherwise prune. OAuth callbacks tie to Workstream 15.
3. `file`: keep only if document/avatar upload is on the roadmap (ties to Workstream 7);
otherwise prune.
4. `search`: prune unless a search UI is planned.
5. After any prune, re-run backend `typecheck`/`lint`, and regenerate
`database-schema.md` only if models change.
Cross-references: Workstream 7 (file upload), 5 (public route audit), 15 (OAuth), 3 (RBAC / permission-based authorization).
Acceptance criteria:
- Every backend endpoint is classified as **used**, **prune**, or **wire (planned)**, with no "unclassified" remainder.
- Pruned routes are removed cleanly (route + service + db/api + permission wiring) with `typecheck`/`lint` green and no dangling imports.
- The frontend remains fully wired (0 orphan calls) after changes.
- This document reflects the final surface.
Performance hardening applied (owner chose **wire**, not prune, for the generic CRUD layer — it will back near-term management UIs modeled on `ref-frontend`):
- `findBy` in all 24 generic-CRUD `db/api/*.ts` now loads its associations via a single `Promise.all` instead of sequential awaited getters (detail-endpoint latency drops from sum to max of the association queries). `users.findBy` was done earlier.
- List pagination now has shared defaults via `@/shared/constants/pagination` (`resolvePagination`, default page size 10 to match the `ref-frontend` grids, capped at 100). Applied to every generic-CRUD `findAll` and to the feature lists (`user_progress`, `safety_quiz_results`, `walkthrough_checkins`, `frame_entries`, `content_catalog`, `communications` parent-messages/events, `campus_attendance` configs), which were reverted from `findAll` back to `findAndCountAll` so `count` is the true total for the pager. `staff_attendance /records` and `campus_attendance /summaries` keep their pre-existing per-endpoint limits.
- Fixed a latent CRUD tenant-scoping bug: routes and `db/api` create/update read `currentUser.organization?.id` (singular), but `findBy` only ever populates `organizations` (plural) + the `organizationId` scalar, so that read was always `undefined` (non-global creates silently set no organization). All 22 `db/api` + 24 route reads now use `currentUser.organizationId`; the dead 3-field fallback term was dropped from the 4 feature services; and the non-existent `organization?: { id }` field was removed from the `CurrentUser` type so the mistake cannot recur.
- The redundant existence-check `findBy` in the 22 CRUD `update` services was removed; they now rely on `DbApi.update` returning `null` (avoids loading every association just to validate existence). `remove` already had no pre-check.
- The per-request `UsersDBApi.findBy` (passport JWT strategy → `req.currentUser`, read by every guarded route) was collapsed from `findOne` + 4 parallel association getters + a `getPermissions()` into a **single** eager-loaded query (`findOne` + `include` of `app_role`+`permissions`, `staff_user`, `custom_permissions`, `organizations`). Its returned `app_role` now carries `permissions`, and `middlewares/check-permissions.ts` was reordered to read that eager-loaded `permissions` array before falling back to `getPermissions()` — removing the extra per-request permissions query. Same returned shape/fields as before (`AuthenticatedUser`); ~67 queries per request → 1. The cached `Public` role still works (its record carries `permissions`).
- `AuthService.currentUserProfile` (the `GET /me`, signin and refresh responses) now uses a dedicated `UsersDBApi.findProfileById` — a single eager-loaded query (`findByPk` + scoped `include`/`attributes`) returning only the columns and relations the profile DTO reads — instead of the heavy generic `findBy` (1 `findOne` + parallel association getters + `getPermissions`) plus a separate `getCampus`. Required idiomatic `NonAttribute` association declarations on the `Users`/`Roles`/`Staff` models so the include is type-safe without casts. The per-request passport `findBy` that populates `req.currentUser` is unchanged (it is the auth gate read by every guard); `/me` still performs that auth fetch plus this one lean profile query.
### 7. File Upload And Download Permissions
Status: open. Permission-gated surface; aligns with Workstream 3 §3.2/§3.5 and Workstream 6 (`file` extras).
Problem:
Backend file and document routes exist. Product file workflows need explicit permission verification before new upload/download UI is added.
Required work:
1. Audit upload permissions.
2. Audit download permissions.
3. Verify tenant and document ownership checks.
4. Add typed frontend upload client only for approved workflows.
Acceptance criteria:
- Unauthorized users cannot access another tenant's files.
- Upload/download behavior is documented and tested before new UI is added.
**Phase 3 — Verification & Quality (locks in correctness once the access model and fixtures exist).**
### 8. Backend-Seeded Authenticated E2E
Status: depends on Workstream 4 fixtures and Workstream 3 guards.
Problem:
Current Playwright coverage includes backend-free smoke tests and backend-seeded content catalog tests. Authenticated persisted workflows need backend-seeded users, roles, campuses, staff profiles, and known credentials.
Required prerequisites:
1. Product onboarding/profile contract (Workstream 3 §3.4).
2. Backend-seeded auth fixtures (Workstream 4).
3. Tenant-scoped campus/staff fixtures (Workstream 4).
4. Stable test credentials stored only in ignored local env or dedicated test seed config.
Required workflows after prerequisites:
1. Login to dashboard.
2. Director creates or edits a FRAME entry and sees it after reload.
3. Staff completes QBS quiz and director/superintendent sees compliance.
4. Office enters campus attendance and superintendent sees aggregate.
5. Director submits walkthrough and sees summary update.
6. Staff marks a sign learned and progress persists after reload.
Acceptance criteria:
- Authenticated e2e tests use backend seeds, not frontend mock data.
- Tests do not require production secrets.
- Tests are documented and repeatable.
### 9. API Documentation Hardening
Status: open. Run after the route surface is finalized (Workstreams 3, 5, 6) so the docs describe the real endpoints.
Problem:
Markdown docs exist for migrated modules, but Swagger/OpenAPI coverage for product-specific and cookie-session endpoints is incomplete.
Required work:
1. Document `/api/auth/signin/local`.
2. Document `/api/auth/refresh`.
3. Document `/api/auth/signout`.
4. Document `/api/auth/me`.
5. Document product module endpoints that are not covered by generated Swagger output.
6. Document response and error shapes per endpoint.
Acceptance criteria:
- API docs match actual route payloads and response shapes.
- Cookie auth behavior is explicit.
- Frontend API contract tests remain aligned with docs.
### 10. Accessibility Test Coverage
Status: open.
Required work:
1. Add axe/Playwright accessibility checks for login.
2. Add axe/Playwright checks for dashboard.
3. Add checks for sidebar navigation.
4. Add checks for modal dialogs.
5. Add checks for forms with validation.
6. Add checks for tables/reports.
Acceptance criteria:
- Accessibility tests run in a documented command.
- Critical violations block completion of the refactor.
**Phase 4 — Product Contracts (each gated by a customer/provider decision; independent of one another and parallelizable once the access core is in place).**
### 11. Policy And Safety Acknowledgment Persistence
Status: open pending product contract.
Problem:
Policy content is document-backed, but policy/protocol acknowledgments are not yet persisted.
Required decisions:
1. Which policies/protocols require acknowledgment.
2. Which roles must acknowledge.
3. Whether acknowledgments are per document version.
4. Who can report acknowledgment status.
Required work after decision:
1. Add acknowledgment backend model/migration/service/route.
2. Enforce tenant and role scope.
3. Add frontend typed API and business workflow.
4. Add report views only if required.
Acceptance criteria:
- Acknowledgments survive reload.
- Acknowledgment status is tenant-scoped.
- Unauthorized roles cannot view individual acknowledgment records.
### 12. Attendance Source Contracts
Status: partially open.
Current state:
- Campus attendance daily aggregate summaries are implemented for the current UI.
- Staff attendance snapshot/reporting is read-only.
- Student/class attendance source-of-truth workflow is not defined.
Open decisions:
1. Whether campus attendance aggregates are manually entered, imported, or derived from student attendance records.
2. Which external or internal source owns staff attendance writes/imports.
3. Whether student-level attendance UI is required.
Required work after decision:
1. Add write/import endpoints only after source contract exists.
2. Keep derived summaries server-side if summaries are derived.
3. Add backend tests for source-of-truth calculations.
4. Add frontend workflows only after backend contracts exist.
Acceptance criteria:
- Attendance source of truth is documented.
- UI values can be traced to backend records or server-side derivation.
- No frontend-only attendance source remains in persisted workflows.
### 13. Generated Audio Provider Contract
Status: open pending provider decision.
Problem:
Classroom timer uses built-in Web Audio sounds. AI-generated audio UI is intentionally not exposed because no backend audio provider contract exists.
No-go rules:
- Do not add generated audio UI.
- Do not call external audio providers from frontend runtime.
- Do not add frontend API keys or provider secrets.
Required work after decision:
1. Define backend provider contract.
2. Keep provider secrets in backend env.
3. Add backend service with native provider errors where required.
4. Add typed frontend API and explicit loading/error states.
Acceptance criteria:
- Generated audio is backend-mediated.
- No provider secret reaches the browser.
- Provider failures remain visible.
**Phase 5 — Operations, Modernization & Cleanup (low coupling; can run any time after Phase 2, scheduled last to avoid mixing with the access-model changes).**
### 14. Refresh Token Maintenance
Status: open.
Problem:
Cookie auth and refresh rotation exist, but scheduled cleanup for expired/revoked refresh-token rows remains unresolved.
Required work:
1. Define refresh-token retention period.
2. Add cleanup job or operational command.
3. Ensure cleanup is observable.
4. Document operational usage.
Acceptance criteria:
- Expired and revoked refresh-token rows are cleaned after the approved retention window.
- Cleanup failures are visible and not silent.
- Auth behavior remains unchanged for valid sessions.
### 15. OAuth Provider Strategy Modernization
Status: open. Deferred until after the backend TypeScript/ESM migration (now complete), to keep auth-flow changes separate from the language/module migration.
Problem:
Social login uses `passport-google-oauth2` (0.2.0, last published 2022) and `passport-microsoft` (2.1.0). The Google strategy is low-maintenance and should be modernized, ideally consolidating both providers on a single maintained OAuth/OIDC library.
Required work:
1. Choose target: minimal swap to `passport-google-oauth20`, or consolidate both providers on `openid-client`.
2. Replace the Google (and optionally Microsoft) passport strategy in `backend/src/auth/auth.ts`.
3. Keep the existing cookie-based session and JWT issuance unchanged.
4. Verify callback URLs, scopes, and the social-signup user flow end to end.
5. Update auth docs (`backend/docs/auth-profile.md`, `cookie-auth.md`).
Acceptance criteria:
- Google and Microsoft sign-in work end to end with the chosen library.
- No deprecated/unmaintained OAuth strategy remains in dependencies.
- Cookie/JWT auth behavior is unchanged for existing sessions.
- This change is isolated from the language/module migration (separate PR/task).
### 16. `ref-frontend/` Removal
Status: open.
Required work:
1. Confirm no active docs require `ref-frontend/` for endpoint contract reference.
2. Confirm no scripts import or run `ref-frontend/`.
3. Delete `ref-frontend/`.
4. Update root docs and any setup docs.
Acceptance criteria:
- Only `frontend/` and `backend/` remain as active application code.
- No docs describe `ref-frontend/` as needed for normal development.
## Phased Execution Order
Run in workstream-number order unless the user explicitly reprioritizes. The phases are the dependency gates; within a phase, items may proceed in parallel.
1. **Phase 1 — Foundation:** Workstream 1 (backend migrates, boots, serves routes).
2. **Phase 2 — Identity, Tenancy & Access Core:** Workstream 2 (tenant isolation) → Workstream 3 (RBAC: roles, permissions, constraints, provisioning, guards) → Workstream 4 (RBAC seed/fixtures), with Workstream 5 (public route audit), Workstream 6 (API surface classify/wire), and Workstream 7 (file permissions) finalizing the protected surface alongside Workstream 3.
3. **Phase 3 — Verification & Quality:** Workstream 8 (authenticated seeded e2e) → Workstream 9 (API docs) → Workstream 10 (accessibility).
4. **Phase 4 — Product Contracts (customer-gated, parallel):** Workstreams 11 (acknowledgments), 12 (attendance source), 13 (generated audio).
5. **Phase 5 — Operations, Modernization & Cleanup:** Workstreams 14 (refresh-token cleanup), 15 (OAuth modernization), 16 (`ref-frontend/` removal).
## Definition Of Done
The integration refactor is complete only when all of the following are true:
- Backend migrations and seeders pass on the configured local database.
- Backend lint passes without broad ignores.
- Backend tenant isolation tests prove cross-tenant data is not visible or mutable.
- The 11-role scoped hierarchy (Workstream 3) is enforced backend-side, including the relational "except …" constraints for `system_admin`, `superintendent`, and `director`; the legacy generated-role mapping and 5-value `productRole` are removed.
- Backend role/permission tests cover product staff, director, and superintendent paths.
- Frontend gating is permission-driven with an authentication guard; unauthenticated users redirect to `/login`, forbidden direct URLs land on 404, and guest sees only public routes.
- The RBAC seed (Workstream 4) provisions one company, one campus with staff covering every campus role, and one loginable user per role, with `admin@flatlogic.com` as the super admin.
- Every unauthenticated backend route is documented as public-by-design.
- Product onboarding contract is implemented per Workstream 3 §3.4 or explicitly excluded from release scope.
- Every visible frontend workflow is backed by a typed backend API contract.
- No frontend runtime workflow depends on mock, sample, seed, or fallback records for persisted behavior.
- Frontend `typecheck`, `lint`, `test`, `build`, `test:e2e`, and documented seeded e2e suites pass in their required environments.
- Backend auth/API docs match actual route behavior.
- Required accessibility checks pass.
- `ref-frontend/` is deleted or a dated, explicit exception explains why it is still needed.