Files
ubi-dan-itam/CLAUDE.md
T
toanhoang 3645e907bc docs: update CLAUDE.md with route-gating status
Moves the middleware/route-gating item out of Viec con do now that
it is done, and documents the auth.config.ts split, matcher choice,
the / home page being protected, and the vi.mock test approach in
Trang thai hien tai.
2026-07-31 12:39:06 +07:00

112 lines
21 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.
# Asset & License Management — Dev Notes
Web app nội bộ quản lý tài sản IT + license cho Ubisoft VN local IT. Luồng chính: Request → Approval → Procurement → Receiving → Delivery → Tracking → Thu hồi/Retire.
Business context, data model, luồng trạng thái, case đặc biệt: xem `docs/asset-license-management.md` — đó là nguồn brainstorm chính, đừng duplicate nội dung đó ở đây, chỉ link tới.
## Stack (đã chốt)
- Next.js (TypeScript) full-stack
- DB: Postgres (đã chốt 2026-07-29, xem lý do trong `docs/asset-license-management.md`)
- DB access: **Prisma** (ORM + migrations, đã chốt 2026-07-30 — lý do trong `docs/asset-license-management.md`). Không viết raw SQL cho app logic nữa — ngoại lệ duy nhất là hạ tầng test (`tests/setup.ts` dùng raw `pg` `Client` để tạo/xoá throwaway database, đó là test infra chứ không phải data access layer).
- Validation: **Zod** (đã chốt 2026-07-30, đi cùng quyết định Prisma)
- UI/CSS: **Tailwind CSS v4 + shadcn/ui (CLI `@latest`, hiện tại v4.16.0)** — nâng cấp từ Tailwind v3 + `[email protected]` ngày 2026-07-30, xem lý do quyết định trong `docs/asset-license-management.md`. Không còn `tailwind.config.ts` — theme khai báo thẳng trong CSS qua `@theme inline { ... }` (`src/app/globals.css`), `postcss.config.js` chỉ còn plugin `@tailwindcss/postcss` (không cần `autoprefixer` riêng nữa). shadcn config: style `new-york-v4`, base color `neutral`, CSS variables bật, `tailwind.config: ""` (`components.json`) — đây là style/preset cổ điển (Radix), **không phải** 8 preset thương hiệu hoá mới của CLI hiện tại (`nova/vega/maia/lyra/mira/luma/sera/rhea`, chọn qua `-p`), vốn ép dùng theme "Nova/Vega..." + tuỳ chọn thư viện primitive khác (`--base base` = Base UI, `--base aria` = React Aria) — không dùng các preset đó cho project này. Component dùng `radix-ui` (package hợp nhất, không phải `@radix-ui/react-slot` riêng lẻ nữa) và `tw-animate-css` (thay `tailwindcss-animate`). `globals.css` còn `@import "shadcn/tailwind.css"` — 1 file CSS runtime do chính package `shadcn` (npm) publish kèm nhiều utility (data-state variants, scroll-fade, shimmer...), là dependency thật (không phải chỉ dev tool) vì cần resolve lúc build. Khi thêm component mới: `npx shadcn@latest add <component>` (đã test non-interactive OK với `components.json` này qua flag `-y --overwrite`).
- Self-hosted (theo nguyên tắc chung của workspace — ưu tiên self-host hơn cloud, xem `../processes/new-office-fitout.md`)
## Coding conventions
Rút ra từ pattern thật đã dùng cho `Request` (xem `src/lib/requests.ts`, `src/app/api/requests/route.ts`, `prisma/schema.prisma`) — theo đúng pattern này cho các entity tiếp theo, đừng bịa pattern mới:
- **1 file schema Prisma** (`prisma/schema.prisma`) cho tất cả model — không tách nhiều file. Field trong model dùng camelCase (`employeeId`), map sang cột DB snake_case qua `@map("employee_id")`; tên bảng map qua `@@map("requests")` (snake_case, số nhiều).
- **1 file `src/lib/<entity>.ts` mỗi entity**, chứa cả 3 thứ trong cùng file: Zod schema + hàm validate trả về `ValidationResult` (`{ok:true,value}|{ok:false,error}`, không throw), các hàm CRUD gọi Prisma Client, và 1 hàm `toRecord()` map từ shape Prisma (camelCase) sang shape public API (snake_case — vd `employee_id`, `created_at`). Ranh giới cố ý: nội bộ TS/Prisma dùng camelCase, API trả ra ngoài dùng snake_case.
- **API route** (`src/app/api/<entity>/route.ts`) giữ mỏng: parse JSON trong try/catch (400 nếu invalid JSON), check `typeof body === "object" && body !== null` (400 nếu không), gọi hàm validate của lib, gọi hàm CRUD, trả `NextResponse.json(...)` với status code đúng (400 validation lỗi, 201 tạo mới, 200 list). Không viết business logic trong route handler.
- **Prisma Client**: dùng singleton ở `src/lib/prisma.ts` (global-guard chống hot-reload spawn nhiều instance) — import `prisma` từ đó, không tự `new PrismaClient()` ở nơi khác.
- **Enum có tập giá trị cố định gắn với domain** (vd `type: asset|license`) → Prisma `enum`, không phải `String` + check tay.
- **Màu/theme Tailwind**: từ khi lên Tailwind v4 (2026-07-30), theme khai báo thẳng trong `src/app/globals.css` qua `@theme inline { --color-*: var(--*); ... }` — không còn `tailwind.config.ts`. Biến CSS ở `:root`/`.dark` đã là giá trị `oklch(...)` đầy đủ, `@theme inline` chỉ alias thẳng qua `var(--x)`, **không bọc thêm** `hsl(...)`/`rgb(...)` (bài học từ bug `hsl(oklch(...))` hồi còn Tailwind v3 + `tailwind.config.ts`, đã fix rồi bỏ luôn file đó khi lên v4).
- **Test**: 1 file `tests/<entity>.test.ts` mirror theo lib file, chạy trên Postgres thật qua DB tạm per-test-file (xem `tests/setup.ts`) — không mock DB. `beforeEach` dọn bằng `prisma.<model>.deleteMany()`.
- Schema mới/đổi schema → tạo migration Prisma thật (`prisma migrate dev --name <mô_tả>`), không sửa tay SQL.
## Git / commit convention
- Message theo [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `docs:`, `chore:`, `test:`, `refactor:` + mô tả ngắn gọn ở thì hiện tại (vd `feat: add Employee Prisma model + migration`).
- **Chia nhỏ commit theo lớp kỹ thuật** khi build 1 entity/feature mới — không gộp hết vào 1 commit lớn khó review. Thứ tự chuẩn cho 1 entity mới:
1. `feat: add <Entity> Prisma model + migration`
2. `feat: add <Entity> validation + CRUD (src/lib/<entity>.ts)`
3. `feat: add <Entity> API routes`
4. `test: add <Entity> API tests`
- Nếu 1 lớp cần sửa lại (bug do lớp sau phát hiện), thêm commit `fix:` riêng, không gộp ngược vào commit cũ (đã push thì không amend).
- Vẫn chạy xuyên suốt cả entity trong 1 lượt (feature-implementer → test-runner) rồi mới hỏi/báo cáo — không dừng lại xin review sau từng commit nhỏ. User sẽ tự xem lại qua `git log`/diff từng commit sau khi xong.
- **Lưu ý PowerShell trên máy này**: đừng để dấu `"` (ngoặc kép) lồng bên trong message nhiều dòng khi truyền qua `git commit -m` — dễ bị PowerShell tách nhầm argument, khiến git báo lỗi pathspec (đã gặp thật, xem cách né ở lịch sử commit). Cách an toàn: gán message vào biến trước (`$msg = @'...'@; git commit -m $msg`), tránh dấu `"` trong nội dung, hoặc dùng `git commit -F <file>` nếu bắt buộc phải có `"`.
## Môi trường
Project này được dùng từ **nhiều máy khác nhau** (máy Windows công ty, VM Linux ở nhà qua Proxmox) — **luôn kiểm tra host hiện tại trước khi giả định cách chạy npm**, đừng áp cứng 1 cách cho mọi nơi.
- **Máy Windows công ty** (`C:\Users\thoangminh\Desktop\ubisoft\asset-license-mgmt`): **không có Node.js/npm cài native**. Mọi `npm install`/`npm test`/`tsc` phải chạy qua Docker (`node:20` container, xem cú pháp bên dưới). Docker Desktop đã confirm hoạt động được trên máy này (dùng để chạy Xibo containers, xem `../xibo/`).
- **VM Linux ở nhà (Proxmox)**: nếu VM có Node.js cài native (kiểm tra bằng `node -v`/`which node` trước), chạy `npm install`/`npm test`/`tsc` **trực tiếp**, không cần wrap qua Docker — chỉ Postgres mới cần Docker (`docker compose up -d db`), vì đó là quyết định kiến trúc (Postgres qua container), không phải vì thiếu Node. Nếu VM đó cũng không có Node, dùng lại cách wrap Docker như máy Windows.
- DB luôn là Postgres qua `docker-compose.yml` (service `db`, network `asset-license-mgmt-net`, image `postgres:16-alpine`) — **cái này giống nhau ở mọi máy**, không phụ thuộc host có Node native hay không. Trước khi `npm test`, phải start Postgres: `docker compose up -d db` (từ thư mục project).
- Nếu phải wrap qua Docker (máy không có Node native): chạy npm trong container gắn cùng network để resolve được hostname `db`:
`docker run --rm --network asset-license-mgmt-net -v "<project dir>:/app" -w /app -e DATABASE_URL="postgres://asset_license_mgmt:change-me-local-dev@db:5432/asset_license_mgmt" node:20 sh -c "npm install && npm test"`
- Nếu Node native: set `DATABASE_URL` (xem `.env`/`.env.example`) rồi chạy thẳng `npm install && npm test` — Postgres vẫn phải resolve qua `db` nếu chạy trong Docker network, hoặc `localhost`/port map nếu compose có publish port (hiện `docker-compose.yml` **không** publish port ra host, cần thêm port mapping hoặc chạy npm trong cùng Docker network nếu muốn gọi từ ngoài container).
- Test tự tạo/xoá một database Postgres tạm (`test_<uuid>`) riêng cho mỗi lần chạy file test (xem `tests/setup.ts`), không đụng vào database dev chính.
- `DATABASE_URL` mẫu nằm ở `.env.example` — copy thành `.env` (đã gitignore) nếu cần chỉnh local, riêng cho từng máy.
- `npm install` tự chạy `prisma generate` qua hook `postinstall` (xem `package.json`) — không cần gọi tay trừ khi thay đổi `schema.prisma` mà không chạy `npm install` lại.
- `pg` vẫn còn trong `devDependencies` — **chỉ** dùng cho hạ tầng test (`tests/setup.ts` tạo/xoá throwaway DB), không phải data access layer của app (đó là Prisma). Đừng nhầm là app vẫn dùng raw SQL.
### Lưu ý về việc gọi custom agent (feature-implementer/test-runner/code-reviewer)
Các agent định nghĩa ở `.claude/agents/` trong thư mục **này** (`asset-license-mgmt/`) chỉ được Claude Code nhận diện làm `subagent_type` nếu phiên Claude Code được mở với working directory **là chính thư mục này** (hoặc thư mục con của nó) — Claude Code quét `.claude/agents/` từ cwd đi lên tới repo root, **không quét xuống** thư mục con. Nếu mở Claude Code từ `ubisoft/` (thư mục cha, gộp nhiều project), 3 agent này sẽ báo "not found". Cách đúng: `cd` vào `asset-license-mgmt/` rồi mới chạy `claude` khi làm việc trên project này.
## Multi-agent dev workflow
Mục tiêu: không để một agent vừa viết code vừa tự chấm bài mình — tách vai trò để bug bị bắt lại thay vì được tự "cho qua".
Với mỗi task cụ thể (vd "thêm API tạo Request", "màn hình bàn giao asset"):
1. **`feature-implementer`** — nhận đúng 1 task, viết code + test đi kèm trong cùng lượt. Không tự tuyên bố "đã pass" — chỉ báo cáo đã đổi gì, giả định gì, và cái gì cố tình để ngoài scope.
2. **`test-runner`** — spawn **riêng**, không phải cùng agent vừa code. Chạy toàn bộ test suite + lint + type-check, báo lỗi cụ thể kèm file:line. Không tự sửa code.
3. Nếu fail: đưa report của test-runner làm input cho một lượt `feature-implementer` mới để fix. Lặp lại bước 1–2 tới khi xanh hết.
4. **`code-reviewer`** (optional, dùng cho thay đổi lớn/rủi ro — vd đổi state machine của Request, logic seat-count license) — review diff cuối cùng trước khi commit, tìm bug logic/security, không tự sửa.
### Quy tắc cứng
- Implementer và test-runner luôn là 2 lượt spawn riêng biệt cho cùng 1 task — không gộp.
- test-runner không được tự sửa file, kể cả khi lỗi trông "hiển nhiên".
- Nếu implementer thấy task mơ hồ, chọn giả định nhỏ nhất hợp lý và ghi rõ trong report thay vì dừng lại hỏi — nhưng nếu ảnh hưởng luồng trạng thái Request/quyền approval thì phải hỏi trước khi code.
## Trạng thái hiện tại
- **Task 1 xong**: đã bootstrap Next.js (TS, App Router) project thật, entity `Request` (chỉ state `Draft`) với `POST/GET /api/requests`.
- **Migrate SQLite → Postgres xong**: CONFIRMED pass qua test-runner độc lập.
- **ESLint setup xong**: CONFIRMED pass sạch qua test-runner độc lập.
- **Retrofit `Request` sang Prisma + Zod xong (2026-07-30)**: `prisma/schema.prisma` (model `Request`, enum `RequestType`), `src/lib/prisma.ts` (singleton client), `src/lib/requests.ts` (Zod schema thay hàm validate tay, Prisma Client thay raw SQL), `scripts/migrate.ts` (gọi `prisma migrate deploy`), `tests/setup.ts`/`tests/requests.test.ts` (schema áp qua `prisma migrate deploy`, dọn dữ liệu qua `prisma.request.deleteMany()`). `src/lib/db.ts` (raw pg Pool cũ) đã xoá. **CONFIRMED qua test-runner độc lập**: 6/6 test pass trên Postgres thật, type-check sạch, lint sạch. Migration SQL đối chiếu khớp 1:1 với schema cũ (chỉ khác `type` từ `TEXT+CHECK` sang Postgres enum — tương đương semantics).
- `npm audit`: 1 "critical" nằm ở `vitest` (devDependency, CVE cần `vitest --ui` đang chạy — project không dùng UI mode nên không khai thác được trong cách dùng hiện tại). Rủi ro thực tế thấp, chưa cần fix gấp — xem "Việc còn dở".
- **Tailwind + shadcn/ui setup xong (2026-07-30, ban đầu trên v3)**: `postcss.config.js`, `src/app/globals.css`, `components.json`, `src/lib/utils.ts` (`cn()`), `src/components/ui/button.tsx` (component mẫu để xác nhận pipeline chạy được). Có 1 bug phát hiện qua verify độc lập (màu token bị bọc `hsl()` chồng lên giá trị `oklch()` sẵn có → CSS không hợp lệ, silent fail lúc render dù build/lint/tsc đều xanh) — đã fix và CONFIRMED qua verify độc lập lần 2.
- **Nâng cấp lên Tailwind v4 + shadcn CLI `@latest` xong (2026-07-30)**: bỏ `tailwind.config.ts` (theme giờ khai trong `globals.css` qua `@theme inline`), đổi `postcss.config.js` sang `@tailwindcss/postcss`, đổi dependency (`radix-ui` thay `@radix-ui/react-slot`, `tw-animate-css` thay `tailwindcss-animate`, thêm `shadcn` làm runtime dependency vì `globals.css` import `shadcn/tailwind.css`), `button.tsx` viết lại theo registry `new-york-v4` hiện tại. **CONFIRMED qua test-runner độc lập**: install/build/tsc/lint sạch, 6/6 test Request vẫn pass (không liên quan tới UI), CSS đã compile xác nhận màu sạch (không còn kiểu bug `hsl(oklch(...))`), không còn gói v3 sót lại.
- **Rủi ro tồn đọng (dormant, chưa gãy)**: `button.tsx` mới bỏ `React.forwardRef` (convention của registry hiện tại là cho React 19), nhưng project đang pin **React 18.3.1** — nếu sau này có chỗ truyền `ref` vào `<Button>`, ref sẽ không được forward đúng cách. Hiện tại không chỗ nào trong code truyền `ref` nên chưa phát sinh lỗi — nhưng cần nhớ khi build UI thật, tránh `<Button ref={...}>` hoặc phải tự thêm lại `forwardRef` nếu cần.
- **Git repo khởi tạo + push lên GitHub xong (2026-07-30)**: `https://github.com/toandev96/ubi-dan-itam.git` (private), remote `origin`/branch `master`. Business context (`docs/asset-license-management.md`) đã move từ `ubisoft/processes/` vào trong repo này cùng ngày, để travel theo code khi clone ở máy khác (vd VM Linux ở nhà qua Proxmox) — không còn phụ thuộc thư mục `processes/` ở workspace ngoài repo nữa cho riêng phần business context của app này.
- **Auth (login nội bộ, không phải SSO công ty) xong (2026-07-31)**: `User` Prisma model (`prisma/schema.prisma`), `src/lib/users.ts` (Zod + `createUser()` + `verifyUserCredentials()`, bcryptjs hash), `scripts/create-user.ts` (tạo account bằng CLI, không có self-registration), Auth.js v5 (`next-auth@^5.0.0-beta.32`, Credentials provider, JWT session, không dùng `@auth/prisma-adapter` vì không cần OAuth/DB session) ở `src/auth.ts` + `src/app/api/auth/[...nextauth]/route.ts`, trang login `src/app/login/page.tsx`. Permission model **flat** (bất kỳ User nào login cũng làm mọi thứ được — quyết định đã chốt, không có `role`). **CONFIRMED qua verify độc lập** (test-runner + security review): 16/16 test pass (6 cũ + 10 mới) trên Postgres thật, tsc/lint/build sạch, không lộ password, bcrypt compare đúng chuẩn constant-time, `AUTH_SECRET` fail-closed thật khi thiếu (đã test thực tế, không fallback giá trị đoán được).
- **Finding nhỏ (chưa block, cần fix sau)**: timing side-channel trong `verifyUserCredentials` — trả về sớm khi email không tồn tại (bỏ qua bcrypt compare), nên request với email sai vs password sai có thời gian phản hồi khác nhau chút, có thể dò được email nào tồn tại. Fix: luôn chạy `bcrypt.compare()` kể cả khi không tìm thấy user (so với 1 hash giả cố định).
- **Finding nhỏ (chấp nhận được, cần track)**: `scripts/create-user.ts --password <giá trị>` lộ password vào shell history/process list — chấp nhận vì chỉ IT admin tự chạy tay, không phải self-registration công khai, nhưng nên cải thiện sau (đọc từ prompt/stdin thay vì argv).
- **Note triển khai (chưa cần ngay, nhớ trước khi deploy)**: chạy `next start` (production) cần thêm `AUTH_TRUST_HOST=true` hoặc `AUTH_URL` trong env, nếu không sẽ gặp lỗi `UntrustedHost` — chưa thêm vào `.env.example`, cần làm trước khi deploy thật ở nhà.
- **Route-gating xong (2026-07-31)**: `src/middleware.ts` (protect mọi page route theo default-deny, redirect `/login` nếu chưa đăng nhập, trừ `/login` chính nó — tránh vòng lặp redirect), `src/auth.config.ts` (config Auth.js edge-safe, tách ra khỏi `src/auth.ts` — **không** có Credentials provider vì `authorize()` gọi `verifyUserCredentials()` kéo theo `bcryptjs`, là Node API không chạy được trên Edge Runtime mà `middleware.ts` dùng; `src/middleware.ts` tự tạo instance `NextAuth(authConfig)` riêng, nhẹ, chỉ để decode JWT session cookie — xác nhận qua build thật: trước khi tách, `next build` báo warning bcryptjs/jose bundle vào Edge Runtime (middleware 115kB); sau khi tách, build sạch, middleware còn 78.9kB). `src/auth.ts` giờ spread `authConfig` + thêm Credentials provider thật, dùng cho API route/Server Component (chạy Node runtime).
- `config.matcher` của middleware loại trừ toàn bộ `/api` (không chỉ `/api/auth`) — API route tự check `auth()` lấy session ngay trong handler (không dựa hoàn toàn vào middleware, theo đúng khuyến nghị chính thức của Auth.js: verify session càng gần chỗ fetch data càng tốt). `/api/requests` (`GET`/`POST`) đã thêm check `auth()`, trả `401 {"error":"Unauthorized"}` nếu chưa login — xem `src/app/api/requests/route.ts`.
- `/` (home page placeholder) **có** yêu cầu login — chọn theo nguyên tắc "mặc định chặn hết, allowlist rõ ràng cho cái không cần" thay vì opt-in dễ quên route mới.
- Test: `tests/requests.test.ts` mock `@/auth` (`vi.mock`) thay vì gọi `auth()` thật, vì `auth()` không tham số đọc cookie qua `next/headers` — chỉ hoạt động khi chạy trong request-handling thật của Next.js, không phải khi test import gọi thẳng `GET()`/`POST()` (bypass hoàn toàn Next). Test mặc định mock session hợp lệ (`beforeEach`) để không phá test nghiệp vụ cũ, có riêng 2 test set mock về `null` để xác nhận 401. `middleware.ts` bản thân **không** có test tự động (chạy Edge Runtime, không test được qua vitest theo cách hiện tại) — đã verify tay qua `next build` + `next start` thật (curl xác nhận: `GET /` → 307 → `/login`, `GET /login` → 200 không loop, `GET/POST /api/requests` chưa login → 401 JSON, `GET /api/auth/session` → 200 không bị chặn). **CONFIRMED**: `npx tsc --noEmit`, `npm run lint`, `npm test` (18/18, gồm 2 test 401 mới), `npm run build` đều sạch.
## Việc còn dở
- Fix timing side-channel ở `verifyUserCredentials` (xem note ở "Trạng thái hiện tại").
- Cải thiện `scripts/create-user.ts` để password không lộ qua CLI arg (đọc từ stdin/prompt thay vì `--password`).
- Thêm `AUTH_TRUST_HOST`/`AUTH_URL` vào `.env.example` trước khi deploy production.
- Bump `vitest` lên 4.x (fix advisory critical GHSA-5xrq-8626-4rwp — hiện không khai thác được vì project không chạy `vitest --ui`, nhưng nên dọn khi có breaking-change window) — không khẩn cấp.
- Rà lại advisory riêng của `next` (prod dependency, có vài high/moderate — vd SSRF qua WebSocket upgrade, middleware bypass) khi có dịp, tách biệt khỏi đợt retrofit Prisma/Zod này.
- Test coverage gaps (chưa block gì, chỉ là follow-up): chưa test malformed JSON body, chưa test JSON body không phải object (array/string).
- `Button` (shadcn) không `forwardRef`, React đang pin 18 — dormant risk, xem note ở "Trạng thái hiện tại".
- Các entity còn lại: `Employee`, `AssetItem`, `License`, `PurchaseOrder`, `Receipt`, `Assignment`
- Các state tiếp theo của Request: Pending Approval → Approved → Ordered → Received → Delivered → Closed, và nhánh Rejected/Cancelled
- UI (hiện chỉ có placeholder home page)
- Auth/phân quyền (ai được approve request, ai được xác nhận nhận hàng/bàn giao)
---
Cập nhật lần cuối: 2026-07-31