diff --git a/.trellis/spec/backend/drizzle-postgres-current.md b/.trellis/spec/backend/drizzle-postgres-current.md new file mode 100644 index 0000000..be1a421 --- /dev/null +++ b/.trellis/spec/backend/drizzle-postgres-current.md @@ -0,0 +1,112 @@ +# Drizzle PostgreSQL Current Persistence Contract + +## Scenario: Current Project After PostgreSQL Migration + +### 1. Scope / Trigger + +- Trigger: implementing or modifying auth, RBAC, audit, elder, facility, admission, organization, settings, or dashboard data flows in this repository. +- Applies because this project already has `drizzle-orm`, `postgres`, Drizzle migrations under `drizzle/`, and schema definitions under `modules/core/server/schema.ts`. +- This supersedes local JSON persistence for current feature work. Local JSON guidance is only historical or for projects that have not adopted Drizzle yet. + +### 2. Signatures + +- `getDatabase(): AppDatabase` +- `checkDatabaseConnection(): Promise<{ ok: boolean; reason: string }>` +- `readData(): Promise` +- `recordAuditLog(input: AuditInput): Promise` +- `requirePermission(permission: Permission, auditContext: DeniedAuditContext): Promise` +- Route Handlers use standard `GET`, `POST`, `PATCH`, and `DELETE` exports and return `Response`. + +### 3. Contracts + +- Environment key: `DATABASE_URL` is required for database-backed runtime behavior. +- Drizzle source of truth: `modules/core/server/schema.ts`. +- Migration output: `drizzle/`. +- Database config: `drizzle.config.ts`. +- Session cookie: + - Name: `teatea_session` + - Flags: `httpOnly`, `sameSite: "lax"`, `path: "/"` + - Lifetime: 7 days +- API response shape: + +```ts +type ApiResult> = + | ({ success: true; reason: string } & T) + | { success: false; reason: string }; +``` + +- `modules/core/server/store.ts` is a compatibility read model: + - `readData()` may aggregate Drizzle rows for existing pages. + - `writeData()` and `updateData()` must remain unavailable after migration. + - New mutation code must use Drizzle queries or transactions. + +### 4. Validation & Error Matrix + +- Missing `DATABASE_URL` -> throw during `getDatabase()` and surface a clear database configuration failure. +- Missing/expired session -> `401` with `{ success: false, reason: "未登录或会话已过期" }`. +- Missing permission -> `403` with `{ success: false, reason: "权限不足" }` and a denied audit log. +- Invalid JSON body -> `400` with a Chinese user-facing `reason`. +- Missing record by ID -> `404` with `{ success: false, reason: "不存在" }`. +- Duplicate account email -> `409` with `{ success: false, reason: "账号已存在" }`. +- Bed/admission conflict -> structured failure response and no partial mutation. + +### 5. Good/Base/Bad Cases + +- Good: UI submits to a Route Handler, the handler validates input, calls `requirePermission`, mutates Drizzle tables in a transaction when multiple tables are involved, records audit, and returns `ApiResult`. +- Base: Server Component reads data directly through focused server helpers or the temporary `readData()` compatibility model. +- Bad: New mutation code calls `writeData()` or `updateData()`. +- Bad: New code treats `.data/teatea.json` as the active persistence layer. +- Bad: API auth is enforced only by hidden UI controls. + +### 6. Tests Required + +- `pnpm lint` +- `pnpm type-check` +- `pnpm build` +- When schema changes are made: `pnpm db:generate`, then review generated SQL and run `pnpm db:migrate` against the configured development database. +- Manual or automated integration assertions: + - first setup creates platform admin, organization, organization roles, membership, and cookie session + - protected app route redirects without cookie + - protected app route renders with a valid cookie + - CRUD mutation persists across reload/API list + - denied permission returns `403` and writes an audit log + - admission mutation updates `admissions`, `beds`, and `elders` consistently + +### 7. Wrong vs Correct + +#### Wrong + +```ts +await updateData((data) => { + data.beds.push(newBed); +}); +``` + +#### Correct + +```ts +const database = getDatabase(); +await database.insert(beds).values({ + organizationId, + roomId, + code, + status: "available", +}); +``` + +#### Wrong + +```ts +await database.insert(admissions).values({ organizationId, elderId, bedId }); +await database.update(beds).set({ status: "occupied" }).where(eq(beds.id, bedId)); +``` + +#### Correct + +```ts +await database.transaction(async (transaction) => { + await transaction.insert(admissions).values({ organizationId, elderId, bedId }); + await transaction.update(beds).set({ status: "occupied" }).where(eq(beds.id, bedId)); + await transaction.update(elders).set({ status: "active" }).where(eq(elders.id, elderId)); +}); +``` diff --git a/.trellis/spec/backend/index.md b/.trellis/spec/backend/index.md index 756b94a..72eb49d 100644 --- a/.trellis/spec/backend/index.md +++ b/.trellis/spec/backend/index.md @@ -20,6 +20,7 @@ | [database.md](./database.md) | Drizzle ORM, queries, transactions, SQL patterns | Database operations | | [authentication.md](./authentication.md) | better-auth, sessions, OAuth, protected procedures | Auth-related features | | [logging.md](./logging.md) | Structured logging, Sentry tracing, telemetry | Debugging, observability | +| [drizzle-postgres-current.md](./drizzle-postgres-current.md) | Current project Drizzle/PostgreSQL persistence boundary | Any persisted feature in this repository | | [local-json-mvp.md](./local-json-mvp.md) | Local JSON persistence/session/API contracts before database adoption | Single-repo MVP features without DB/oRPC deps | | [deployment.md](./deployment.md) | Wulanchabu deployment target and validation contract | Deploying this project | | [performance.md](./performance.md) | Concurrency, caching, batch processing, streaming | Performance optimization | diff --git a/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/design.md b/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/design.md index d31725f..e6a3408 100644 --- a/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/design.md +++ b/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/design.md @@ -2,112 +2,156 @@ ## Architecture -This task keeps the app in a single Next.js project and adds a small server-side domain layer inside `modules/`: +This task keeps the app in a single Next.js project and continues the Drizzle/PostgreSQL-backed server domain layer inside `modules/`: -- `modules/core/server/`: server-only persistence, session, permissions, and audit helpers. -- `modules/auth/`: UI and client interactions for setup, login, logout, and session state. -- `modules/elders/`: elder schemas/types, server actions or API client helpers, and UI components. -- `modules/settings/`: role/account/audit display components. -- `app/api/.../route.ts`: Route Handlers for auth, session, elders, accounts, and audit APIs. +- `modules/core/server/`: Drizzle connection, schema, session, permissions, audit helpers, and compatibility read models. +- `modules/auth/`: UI and client interactions for setup, register, login, logout, and session state. +- `modules/elders/`: elder schemas/types, validation, and CRUD UI components. +- `modules/settings/`: role/account/organization/audit/status display and management components. +- `modules/facilities` or existing page-local modules: room, bed, occupancy, admission, transfer, and discharge UI as the feature grows. +- `app/api/.../route.ts`: Route Handlers for auth, session, elders, facilities, admissions, accounts, roles, organizations, status, and audit APIs. -The persistence boundary should be centralized behind helper functions that read/write JSON files. UI and route handlers must not know file paths directly. +The persistence boundary is now Drizzle/PostgreSQL, not JSON files. UI and route handlers should use domain helpers or direct Drizzle queries through `getDatabase()` in server-only modules. New mutation paths must not use `writeData()` or `updateData()`. ## Data Storage -Use a JSON file store under `.data/teatea.json` by default. The file should contain: +The current persistent model is PostgreSQL with Drizzle schema in `modules/core/server/schema.ts` and generated migrations under `drizzle/`. -```ts -type AppData = { - accounts: Account[]; - sessions: Session[]; - elders: Elder[]; - auditLogs: AuditLog[]; -}; -``` +Important existing tables: -The store module owns initialization, read, write, and update operations. Updates should read the current snapshot, apply a synchronous mutation callback, and write the full file back. This is enough for the MVP and gives a clear future replacement boundary for Drizzle/Postgres. +- Identity and tenancy: `accounts`, `sessions`, `organizations`, `memberships`, `join_requests`, `organization_invitations`. +- Authorization: `roles`, `permissions`, `role_permissions`. +- Operations: `elders`, `rooms`, `beds`, `admissions`. +- Governance and status: `audit_logs`, `system_incidents`, `system_settings`. + +`modules/core/server/store.ts` should be treated as a compatibility read model only: + +- `readData()` may aggregate Drizzle rows into existing UI-friendly types while legacy pages are migrated. +- `writeData()` and `updateData()` intentionally throw after the PostgreSQL migration. +- New feature work should prefer focused query helpers so the compatibility model does not become the long-term domain layer. ## API Contracts Route Handlers return a consistent response shape: ```ts -type ApiResult = +type ApiResult> = | ({ success: true; reason: string } & T) | { success: false; reason: string }; ``` -Planned endpoints: +Existing or expected endpoints: - `GET /api/auth/bootstrap`: returns whether setup is required. -- `POST /api/auth/setup`: creates the first admin account, creates a session, logs setup. +- `POST /api/auth/setup`: creates the first platform admin, first organization, session, roles/membership, and audit log. +- `POST /api/auth/register`: creates a user registration or invitation-based account flow where enabled. - `POST /api/auth/login`: validates credentials, creates a session, logs login. - `POST /api/auth/logout`: deletes current session cookie/session, logs logout. -- `GET /api/auth/session`: returns current account and permissions. -- `GET /api/elders`: lists elder profiles. -- `POST /api/elders`: creates an elder profile. +- `GET /api/auth/session`: returns current account, organization, membership, and permissions. +- `GET /api/elders`: lists elder profiles for the active organization. +- `POST /api/elders`: creates an elder profile and optional active admission. - `PATCH /api/elders/[id]`: updates an elder profile. - `DELETE /api/elders/[id]`: deletes an elder profile. +- `GET /api/facilities/rooms`: lists rooms for the active organization. +- `POST /api/facilities/rooms`: creates room records when facility management UI exposes this. +- `GET /api/facilities/beds`: lists beds and current occupancy. +- `POST /api/facilities/beds`: creates bed records when facility management UI exposes this. +- `GET /api/admissions`: lists admission and transfer history. +- `POST /api/admissions`: admits or transfers an elder into an available bed. +- Planned: `PATCH /api/admissions/[id]` or equivalent mutation endpoint for transfer/discharge if POST cannot express the workflow cleanly. - `GET /api/settings/accounts`: lists accounts for authorized roles. -- `GET /api/settings/roles`: lists built-in role definitions. +- `GET /api/settings/roles`: lists built-in and organization roles. +- `GET /api/settings/permissions`: lists permission coverage. - `GET /api/audit-logs`: lists recent audit events. ## Authentication -Sessions use an HTTP-only cookie. Route Handlers read and write cookies with `await cookies()` from `next/headers`, matching current Next.js behavior. +Sessions use an HTTP-only cookie named `teatea_session`. Route Handlers read and write cookies with `await cookies()` from `next/headers`, matching current Next.js behavior. -Passwords are never stored in plain text. For the MVP, use Node built-in crypto with a per-account salt and `scryptSync` or `scrypt` for password hashing. This avoids adding a dependency while keeping the current local milestone meaningfully better than browser-only auth. +Passwords are never stored in plain text. The current implementation uses Node built-in crypto with per-account salt and `scryptSync`, which is acceptable for this local MVP until a dedicated auth library is introduced in a separate task. + +Protected app routes should use server session state where practical instead of client-only localStorage guards. Server-rendered pages that depend on session cookies must opt out of static prerendering with `export const dynamic = "force-dynamic"` where needed. ## Permissions -Define built-in role permissions in one server/shared constants module: +Permissions and role definitions live in the core server/shared type boundary: -- `account:read` -- `account:manage` -- `audit:read` -- `elder:read` -- `elder:create` -- `elder:update` -- `elder:delete` +- Platform: `platform:manage`, `organization:read`, `organization:manage`. +- Account/role/security: `account:read`, `account:manage`, `role:read`, `role:manage`, `permission:read`, `audit:read`. +- Operations: `facility:read`, `facility:manage`, `admission:read`, `admission:manage`, `elder:read`, `elder:create`, `elder:update`, `elder:delete`. +- Status: `incident:read`, `incident:manage`. -Route Handlers call a shared `requirePermission(permission)` helper. This helper returns an authenticated context or a structured forbidden/unauthorized result and writes denied audit entries. +Route Handlers call a shared `requirePermission(permission)` helper. This helper returns an authenticated context or a structured forbidden/unauthorized response and writes denied audit entries. -## Audit Logging +## Admission Transactions -Audit logging is implemented as a server helper that appends immutable records to the store: +Bed/admission mutations must run inside a Drizzle transaction. -- timestamp -- actor account ID/email if known -- action -- target type -- target ID -- result: `success` or `denied` or `failure` -- reason +Admit flow: -Audit writes are part of the route handler flow. The MVP accepts best-effort logging for logout when a session has already expired. +1. Validate active organization and `admission:manage`. +2. Validate elder belongs to the active organization. +3. Validate target bed belongs to the active organization and has status `available`. +4. Close or transfer any active admission for that elder if the operation is a transfer. +5. Insert a new `admissions` row with status `active`. +6. Set target bed status to `occupied`. +7. Set elder status to `active`. +8. Record an audit log. + +Transfer flow: + +1. Find active admission for the elder. +2. Set previous admission status to `transferred` and `dischargedAt` to now. +3. Set previous bed status to `available`. +4. Insert the new active admission and occupy the target bed. +5. Record an audit log. + +Discharge flow: + +1. Find active admission. +2. Set admission status to `discharged` and `dischargedAt` to now. +3. Set bed status to `available`. +4. Set elder status to `discharged` or another explicitly selected status. +5. Record an audit log. + +All conflict checks must return structured API failures rather than partially mutating state. ## UI Flow -Protected app pages should use server session state where practical instead of a client-only `AuthGate`. The app layout can fetch session data and redirect unauthenticated users before rendering protected content. +The app should remain an operational workspace: dense, restrained, and action-oriented. -The elder page becomes a real data view: +Elder page: -- Server Component loads initial elders. +- Server Component loads initial elders and available beds. - Client component handles create/edit/delete forms and refreshes after mutations. -- Controls are disabled or hidden based on current permissions. +- Controls are disabled or hidden based on current permissions, while APIs still enforce permission checks. -The settings page becomes a server-loaded administrative view: +Bed/admission page: -- Role definitions table. -- Accounts table. -- Recent audit log table. +- Replace raw API placeholders with page-local controls. +- Use tabs or segmented navigation for overview, bed status, admissions/history, and management actions. +- Place admit/transfer/discharge actions inside the bed/admission workspace, not as a dead global top-bar button. +- Show occupancy metrics, active admissions, room/bed tables, and history from Drizzle-backed data. + +Dashboard: + +- Replace hard-coded counters for implemented domains with Drizzle-backed data. +- Use a standard chart library for selected charts such as occupancy distribution or admission activity. +- Keep chart usage modest; tables and status lists remain the primary record surfaces. + +Screenshot feedback: + +- Move page-level secondary navigation into tabs where appropriate. +- Remove oversized intro/hero blocks from routine operational pages. +- Remove redundant header action controls that are not wired to the current page workflow. ## Compatibility -Static module pages not in scope remain untouched except for auth/layout integration. Existing visual design should be preserved: dense operational screens, restrained cards/tables, and Tailwind/Radix-compatible UI components. +Static module pages not in scope remain untouched except for auth/layout integration. Existing visual design should be preserved: dense operational screens, restrained cards/tables, project UI adapters under `components/ui/*`, and Tailwind-compatible layout. ## Risks and Rollback -- File persistence is not safe for high-concurrency production writes. This is acceptable for MVP and documented as a migration boundary. -- Replacing `AuthGate` with server-side auth can affect all `/app` routes. Rollback point: keep the old component until server session redirect is working. -- Password hashing must use Node runtime APIs, so affected Route Handlers should run in the Node runtime if needed. +- Drizzle schema and migrations are now the persistence source of truth; mismatches between schema and migrations can block deployment. Rollback point: keep migration changes separate from UI-only work. +- Admission mutations touch multiple tables. Rollback point: keep transaction helpers isolated and temporarily render read-only admission data if mutation UI is unstable. +- Replacing global header actions can affect user navigation habits. Rollback point: remove only the dead top-bar "入住" button while keeping the sidebar and pages stable. +- Adding a chart library increases client bundle size. Rollback point: limit chart usage to one focused client component and keep tables as the fallback data surface. diff --git a/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/implement.md b/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/implement.md index 726ad45..a7a34d3 100644 --- a/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/implement.md +++ b/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/implement.md @@ -2,76 +2,91 @@ ## Checklist -1. Add core server modules: - - JSON store read/write helpers. - - Account/session/password helpers. - - Role and permission definitions. - - Audit logging helper. - - Shared API response helpers. +1. Reconcile existing Drizzle migration state: + - Confirm `modules/core/server/schema.ts` matches `drizzle/` migrations. + - Confirm `modules/core/server/db.ts` is the only PostgreSQL connection boundary. + - Confirm `modules/core/server/store.ts` is treated as a compatibility read model only. + - Remove task assumptions that mention JSON file writes as the target persistence layer. -2. Add auth APIs: - - `GET /api/auth/bootstrap` - - `POST /api/auth/setup` - - `POST /api/auth/login` - - `POST /api/auth/logout` - - `GET /api/auth/session` +2. Verify existing auth/RBAC/audit slice: + - Confirm setup creates platform admin, organization, session, organization roles, membership, and audit log. + - Confirm login/logout uses server APIs and the `teatea_session` HTTP-only cookie. + - Confirm `requirePermission` logs denied permission checks. + - Confirm roles and permissions cover facility and admission operations. -3. Migrate auth UI: - - Update `AuthPanel` to call server APIs. - - Update `SignOutButton` to call logout API. - - Replace or bypass localStorage-only `AuthGate` with server session protection in the app layout. - - Keep unauthenticated redirects and setup redirects working. +3. Finish elder CRUD on Drizzle: + - Keep validation in `modules/elders/types.ts`. + - Ensure list/create/update/delete APIs query and mutate Drizzle only. + - Ensure elder create with `bedId` performs admission and bed status updates inside a transaction. + - Ensure UI refresh and error handling remain usable. -4. Add elder APIs and UI: - - Create elder types and input validators. - - Implement list/create/update/delete Route Handlers. - - Replace `app/(app)/app/elders/page.tsx` static content with server-loaded CRUD UI. +4. Add or complete bed/admission APIs: + - Keep `GET /api/facilities/rooms` and `GET /api/facilities/beds` Drizzle-backed. + - Keep `POST /api/admissions` transactional for admit/transfer. + - Add discharge and explicit transfer mutation support if the existing POST shape is not enough. + - Return structured `{ success, reason, ... }` responses for conflicts and validation failures. + - Write audit events for admission create, transfer, discharge, and facility mutations implemented in this task. -5. Add settings/audit UI: - - Implement settings/account, role, and audit APIs. - - Replace `app/(app)/app/settings/page.tsx` static content with server-loaded tables. +5. Build bed/admission UI: + - Replace "please create through API" empty states with usable controls where permission allows. + - Add tabs or segmented navigation for overview, bed status, admission actions/history, and facility records. + - Move admission actions into the bed/admission workspace. + - Remove or replace the dead global top-bar "入住" button in `AppShell`. + - Ensure page layout matches screenshot feedback: compact workspace header, no oversized intro block, local page actions. -6. Wire audit events: - - Account setup/create. - - Login/logout. - - Elder create/update/delete. - - Denied permission checks. +6. Introduce selected Phase 2 UI affordances: + - Add tabs to pages that combine overview and management modes. + - Keep implementation incremental; do not attempt every sidebar module. + - Keep user-facing copy operational and concise. -7. Verification: +7. Add standard chart usage: + - Decide the chart library before adding dependency. Recharts is the likely default unless a better project fit is chosen. + - Install the library only after checking existing dependencies. + - Replace at least one meaningful hand-built chart with a data-backed chart component. + - Keep chart component client-only and pass serializable server data into it. + +8. Replace hard-coded operational counters where data exists: + - Dashboard bed/elder/admission counters should come from Drizzle-backed data. + - Bed/admission workspace metrics should compute from server-loaded rooms, beds, and admissions. + - Do not fabricate counters for modules that remain out of scope. + +9. Verification: - Run `pnpm lint`. - Run `pnpm type-check`. - Run `pnpm build`. - - Start dev server and manually exercise setup/login/CRUD/settings if build passes. + - Run Drizzle generation/check commands when schema changes are made. + - Start dev server and manually exercise setup/login/elder CRUD/bed admission/transfer/discharge/settings if build passes. ## Files Expected to Change -- `modules/auth/components/AuthPanel.tsx` -- `modules/auth/components/AuthGate.tsx` -- `modules/auth/components/SignOutButton.tsx` -- `app/(app)/app/layout.tsx` -- `app/(app)/app/elders/page.tsx` -- `app/(app)/app/settings/page.tsx` +- `.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/prd.md` +- `.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/design.md` +- `.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/implement.md` +- `modules/shared/components/AppShell.tsx` +- `app/(app)/app/beds/page.tsx` +- `app/(app)/app/dashboard/page.tsx` +- `modules/dashboard/components/DashboardHome.tsx` +- `app/api/admissions/route.ts` +- Potentially `app/api/admissions/[id]/route.ts` +- Potentially facility UI components under `modules/` if the bed page is split into smaller client/server components. -## Files Expected to Be Created +## Files Expected to Stay As Existing Foundations -- `modules/core/server/store.ts` +- `modules/core/server/db.ts` +- `modules/core/server/schema.ts` - `modules/core/server/auth.ts` - `modules/core/server/permissions.ts` - `modules/core/server/audit.ts` - `modules/core/server/api.ts` -- `modules/elders/types.ts` -- `modules/elders/components/EldersClient.tsx` -- `modules/settings/components/SettingsOverview.tsx` -- `app/api/auth/bootstrap/route.ts` -- `app/api/auth/setup/route.ts` -- `app/api/auth/login/route.ts` -- `app/api/auth/logout/route.ts` -- `app/api/auth/session/route.ts` -- `app/api/elders/route.ts` -- `app/api/elders/[id]/route.ts` -- `app/api/settings/accounts/route.ts` -- `app/api/settings/roles/route.ts` -- `app/api/audit-logs/route.ts` +- `modules/core/server/store.ts` as a temporary Drizzle-backed read model. +- Existing auth route handlers unless verification finds a concrete bug. +- Existing elder route handlers unless admission or bed assignment behavior requires a narrow fix. + +## Dependency Notes + +- Do not add oRPC or better-auth in this task. +- Do not reintroduce JSON-file persistence. +- A standard chart library may be added after dependency review. Prefer a focused dashboard dependency over a broad visualization stack. ## Validation Commands @@ -79,11 +94,18 @@ pnpm lint pnpm type-check pnpm build -pnpm dev +pnpm db:generate +``` + +If schema files are changed, also validate migrations and run the migration flow against the configured development database: + +```bash +pnpm db:migrate ``` ## Rollback Points -- If server auth blocks all app routes, revert only layout/AuthGate changes while keeping APIs. -- If elder CRUD UI is unstable, keep APIs and temporarily render a read-only server table. -- If file store causes build/runtime issues, move the data directory to `/tmp` behind the same store API for verification, then restore `.data` once filesystem assumptions are fixed. +- If admission mutation UI is unstable, keep the Drizzle APIs and temporarily render a read-only admissions table. +- If discharge/transfer API design becomes too broad, implement only admission create/transfer in this slice and keep discharge as a clearly scoped follow-up. +- If chart dependency causes build or bundle issues, remove the chart component and keep the server data preparation in place. +- If tabs cause layout regressions, keep the page-local action placement and defer only the tab styling. diff --git a/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/prd.md b/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/prd.md index 4a520f9..2812057 100644 --- a/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/prd.md +++ b/.trellis/tasks/07-01-nextjs-fullstack-crud-rbac-audit/prd.md @@ -1,57 +1,94 @@ -# Next.js Full-Stack CRUD, RBAC, and Audit Logs +# Next.js Drizzle Full-Stack CRUD, RBAC, Audit Logs, and Admission Ops ## Goal -Turn the current static/local-storage Next.js app into a minimal real full-stack application for the first operational slice: account setup/login, built-in RBAC, elder profile CRUD, and auditable administrative actions. +Stabilize the current Next.js 15 application around the Drizzle/PostgreSQL persistence layer that has already been introduced, then finish the first operational slice and prepare a controlled Phase 2 path for bed/admission operations, tabs-based workspace navigation, and real chart components. -The first release should remove mock/local-only behavior from the core flow and persist data through server-side APIs so the app can be exercised end-to-end without browser localStorage state. +The near-term release should keep moving away from static/mock-only surfaces, but it must treat the existing Drizzle schema and migrations as the source of truth. JSON-file persistence is no longer the target architecture for this task. ## Confirmed Facts - The repository is already a Next.js 15 App Router project with routes under `app/`. -- Protected app screens are currently guarded by `modules/auth/components/AuthGate.tsx`, which reads localStorage through `modules/auth/lib/local-auth.ts`. -- Login/register/setup UI exists in `modules/auth/components/AuthPanel.tsx`, but password input is not validated server-side and accounts are browser-local. -- The "老人档案" route at `app/(app)/app/elders/page.tsx` is currently a static `ModulePage`. -- The "权限设置" route at `app/(app)/app/settings/page.tsx` is currently a static `ModulePage`. -- The project has no installed database/auth dependencies such as Drizzle, PostgreSQL client, oRPC, Zod, or better-auth. -- Current `package.json` already supports `pnpm lint`, `pnpm type-check`, and `pnpm build`. +- Drizzle and PostgreSQL dependencies are installed: `drizzle-orm`, `drizzle-kit`, and `postgres`. +- Drizzle schema exists at `modules/core/server/schema.ts`, with migrations under `drizzle/`. +- The schema already includes accounts, sessions, organizations, memberships, roles, permissions, elders, rooms, beds, admissions, audit logs, system incidents, invitations, and system settings. +- `modules/core/server/db.ts` owns the Drizzle/PostgreSQL connection and requires `DATABASE_URL`. +- `modules/core/server/store.ts` is now a Drizzle-backed compatibility read model. `readData()` aggregates data from PostgreSQL, while `writeData()` and `updateData()` intentionally throw after the PostgreSQL migration. +- Server-side authentication, password hashing, HTTP-only session cookies, role seeding, permission checks, and audit logging are implemented around Drizzle. +- Elder CRUD APIs at `app/api/elders/*` write through Drizzle and can create an active admission when a bed is selected. +- Facility and admission routes exist for rooms, beds, and admissions, but the bed/admission UI is still mostly read-only and does not yet provide a complete operator workflow. +- The dashboard still contains hard-coded operational counters and hand-built chart-like progress bars. +- The app shell has a top-bar "入住" button, but it is not wired to a usable admission workflow. +- The screenshot feedback requires moving workspace-level navigation into tabs, removing oversized module intro blocks, and avoiding redundant header actions such as the extra "入住" affordance in the top bar. +- Current `package.json` already supports `pnpm lint`, `pnpm type-check`, `pnpm build`, `pnpm db:generate`, `pnpm db:migrate`, and `pnpm db:studio`. ## MVP Scope -- Implement a real server-side persistence layer using JSON files under a server-owned data directory for this milestone. This is not mock data: API mutations must write durable data on disk during local/runtime execution. -- Use Next.js Route Handlers for the first API surface instead of adding oRPC/Drizzle/better-auth in this milestone. -- Replace localStorage authentication with server-side account/session APIs and HTTP-only cookie sessions. -- Provide built-in roles and permission groups without user-defined custom role creation in this milestone. -- Implement full CRUD for elder profiles as the first business entity. -- Implement a permissions/settings page that shows accounts, roles, permission coverage, and audit logs from server data. -- Record audit log entries for login, logout, account creation, elder create/update/delete, and permission-sensitive denied actions. +- Use the existing Drizzle/PostgreSQL layer for all new and changed persistent business data. +- Keep Next.js Route Handlers as the current API surface for this milestone. Do not introduce oRPC or better-auth in this task unless a separate migration task is created. +- Preserve server-side account/session APIs, HTTP-only cookie sessions, built-in platform and organization roles, membership permissions, and audit logging. +- Complete full CRUD for elder profiles as the first business entity, backed by Drizzle. +- Complete basic bed and admission operations: list rooms/beds, show current occupancy, create occupancy/admission, support bed transfer, and keep bed/admission/elder status consistent in a transaction. +- Implement an operator-friendly bed/admission UI under the app workspace instead of exposing "create through API" placeholders. +- Add workspace tabs where the current UI needs secondary navigation, especially for operational pages that combine overview, records, and management views. +- Replace hard-coded counters on implemented areas with Drizzle-backed data. +- Use a standard React chart library for selected operational charts instead of hand-rolled visual bars where the visualization is meaningful and data-backed. +- Keep settings pages showing accounts, roles, permission coverage, organizations, status, and audit logs from server data. +- Record audit log entries for login, logout, account creation, elder create/update/delete, admission create/transfer/discharge, facility mutations, and permission-sensitive denied actions. ## Requirements ### Authentication -- Setup must create the first administrator account on the server. +- Setup must create the first platform administrator account, initial organization, initial organization membership, and cookie session on the server. - Login must validate account credentials on the server and set an HTTP-only session cookie. - Logout must clear the session cookie and record an audit event when possible. -- App routes must no longer depend on localStorage for authentication. +- App routes must not depend on localStorage for authentication. ### Roles and Permissions - The system must include built-in roles: - - `admin`: full access to accounts, permissions, audit logs, and elder CRUD. - - `manager`: elder CRUD and audit log viewing, but no account/role administration. - - `caregiver`: elder read/update access for care-facing fields, no delete or account administration. - - `viewer`: read-only elder access. + - `platform_admin`: full platform access. + - `platform_operator`: platform organization/account operations without full security ownership. + - `platform_auditor`: platform read/audit access. + - `platform_ops`: platform operations and incident management. + - `org_admin`: full organization-level access. + - `manager`: elder, facility, admission, and audit operations inside an organization. + - `caregiver`: elder read/update plus read-only facility/admission access. + - `viewer`: read-only elder, facility, and admission access. - Permission checks must run on the server for all protected APIs. - UI navigation or controls may hide unavailable actions, but hidden UI must not be the only enforcement. ### Elder CRUD - Users with permission can list, create, update, and delete elder profiles. -- Elder records must include at minimum: name, gender, birth date or age, care level, room/bed, status, primary contact, phone, medical notes, created/updated timestamps. +- Elder records must include at minimum: name, gender, age, care level, current room/bed when admitted, status, primary contact, phone, medical notes, created/updated timestamps. - The elder page must show real server data and support create/edit/delete interactions. - Invalid input must return structured API errors and show usable feedback in the UI. +### Bed and Admission Operations + +- Operators with `facility:read` can view rooms, beds, occupancy status, current elder assignment, and admission history. +- Operators with `facility:manage` can create or update room and bed metadata where the UI exposes those controls. +- Operators with `admission:manage` can admit an elder to an available bed, transfer an active elder to another available bed, and discharge an elder from a bed. +- Admission mutations must be transactional: active admission records, bed statuses, and elder status must stay consistent. +- Bed assignment conflicts must be rejected with structured API errors. +- The UI must make common workflows discoverable without relying on raw API calls. +- The top app header should not contain redundant or dead "入住" controls. Admission actions should live inside the relevant bed/admission workspace. + +### Workspace UI and Tabs + +- The app shell should keep dense operational navigation and avoid large hero-style module introductions. +- Pages that combine multiple operator modes should use tabs or equivalent segmented navigation, not stacked explanatory sections. +- The screenshot feedback for notices/general workspace applies broadly: remove oversized first-screen intro blocks when they do not help an operator act, place secondary navigation tabs near the workspace header, and keep action controls local to the page. +- UI text should remain operational and data-oriented, not marketing copy. + +### Charts + +- Use a standard chart library for selected data-backed charts such as bed occupancy composition, admission activity, or status distribution. +- Avoid hand-coded fake chart bars for business metrics that should reflect persisted data. +- Keep charts lightweight, readable, and useful for operations; tables remain the primary surface for detailed records. + ### Audit Logs - Audit logs must be persisted server-side and visible in the settings page. @@ -62,43 +99,57 @@ The first release should remove mock/local-only behavior from the core flow and - API responses must use a consistent `{ success, reason, ... }` shape. - Server-side data helpers must avoid browser APIs. -- All file persistence operations must be centralized so future Drizzle/Postgres migration has a single boundary to replace. -- Do not use hard-coded UI counters for the implemented CRUD/settings/audit areas once server data exists. +- New persistent mutations must use Drizzle queries or transactions through the server database boundary, not JSON file writes. +- `readData()` may remain temporarily as a compatibility read model, but new mutation code must not depend on `writeData()` or `updateData()`. +- Prefer domain-specific Drizzle query helpers over growing the compatibility read model for every new use case. +- Do not use hard-coded UI counters for implemented CRUD, settings, audit, bed, admission, or dashboard areas once server data exists. +- Routes or server components that depend on cookies/session or live database state must opt out of static caching where required. ### Quality - Keep TypeScript strict without `any`, non-null assertions, or `@ts-ignore`/`@ts-expect-error`. - Prefer Server Components for initial data loading and small Client Components only for forms/mutations. -- `pnpm lint`, `pnpm type-check`, and `pnpm build` should pass before marking implementation complete. +- `pnpm lint`, `pnpm type-check`, `pnpm build`, and relevant Drizzle migration validation should pass before marking implementation complete. ## Acceptance Criteria -- [ ] First-run setup creates a server-persisted admin account and redirects into the app. +- [ ] First-run setup creates a Drizzle-persisted platform admin account, initial organization, organization roles, membership, and cookie session, then redirects into the app. - [ ] Login/logout uses server APIs and an HTTP-only cookie session, not localStorage. -- [ ] Visiting `/app/elders` while authenticated loads elder records from the server persistence layer. +- [ ] Visiting `/app/elders` while authenticated loads elder records from PostgreSQL through the server layer. - [ ] An authorized user can create, edit, and delete elder records from the UI, and changes survive page reloads. - [ ] Unauthorized role attempts against protected elder/account/audit APIs return a forbidden response and create audit log entries. -- [ ] `/app/settings` displays built-in roles, account list, and recent audit log entries from server data. -- [ ] Audit logs are written for account creation, login, logout, elder create/update/delete, and denied permission checks. +- [ ] Settings pages display built-in roles, account list, organizations, system status, and recent audit log entries from server data. +- [ ] `/app/beds` or the equivalent admission workspace shows Drizzle-backed room, bed, occupancy, and admission data without raw API placeholders. +- [ ] An authorized user can admit an elder to an available bed from the UI. +- [ ] An authorized user can transfer or discharge an active admission from the UI, with bed status and elder status updated transactionally. +- [ ] Admission conflict attempts return structured errors and do not corrupt bed occupancy state. +- [ ] The screenshot feedback is reflected in the workspace layout: tabs are introduced where needed, oversized intro blocks are removed, and redundant top-bar admission action is removed or replaced with a useful page-local action. +- [ ] At least one implemented operational chart uses a standard chart library and real server data. +- [ ] Audit logs are written for account creation, login, logout, elder create/update/delete, admission create/transfer/discharge, facility mutations implemented in this task, and denied permission checks. - [ ] Existing static module pages outside the MVP continue to render. +- [ ] Existing Drizzle migrations remain coherent with `modules/core/server/schema.ts`. - [ ] `pnpm lint` passes. - [ ] `pnpm type-check` passes. - [ ] `pnpm build` passes. ## Out of Scope -- PostgreSQL/Drizzle migration. -- oRPC adoption. -- better-auth adoption. -- Password reset, email verification, OAuth, multi-factor authentication, or account invitations. -- Custom role builder UI. +- Reverting to JSON-file persistence. +- Full historical data migration from any old `.data/teatea.json` files unless a separate migration task is created. +- Replacing the current Route Handlers with oRPC. +- Replacing the current custom session implementation with better-auth. +- Password reset, email verification, OAuth, multi-factor authentication, or new invitation flows beyond what already exists. +- Full custom role builder beyond the existing role/permission management surface. - CRUD implementation for every module in the sidebar. +- Advanced care plan, billing, family app, device telemetry, and emergency workflow automation. - Production-grade password hashing beyond Node built-in cryptographic hashing suitable for this local MVP. ## Open Questions -- None blocking. The MVP assumes "basic CRUD" means the first core business entity, elder profiles, plus real account/session/audit support. +- Which chart library should be standardized for the project? Recharts is a practical default for React dashboards, but the decision should be captured before adding the dependency. +- Should room/bed creation be part of this immediate slice, or should the UI focus first on admission, transfer, and discharge against existing room/bed records? ## Notes - Next.js documentation confirms App Router Route Handlers support `GET`, `POST`, `PATCH`, and `DELETE`, `Response.json`, `request.json()`, and async `cookies()` from `next/headers` for cookie reads/writes in current versions. +- The current codebase already demonstrates the intended Drizzle direction. Future task text should not describe JSON files as the target persistence layer unless explicitly working on a compatibility migration.