API Guide
This is a single Next.js 16 app — there is no separate API server and no OpenAPI spec. The "API surface" is three things: Server-Component data fetching, Server Actions, and Route Handlers.
| Surface | Where | Use for | Cross-origin? |
|---|---|---|---|
| Server Component fetch | app/**/page.tsx (async) | reading data to render a page | No |
Server Action ("use server") | actions/*.ts | mutations from forms/components | No |
| Route Handler | app/api/**/route.ts | webhooks, health, OAuth | Yes |
Route Handlers
Route Handlers live in next-app/app/api/ and follow the App Router route.ts convention. Use them only when something outside the app must reach an HTTP URL.
Health Check
GET /api/healthReturns { status: "ok", timestamp: "...", appInstanceId: "<16-hex>", appName: "瑞成 PMS" } from app/api/health/route.ts. Use this to verify the deployed app is reachable and the interactive API playground is pointed at the right base URL. appInstanceId (E337) is an opaque digest of the server's project path (or the APP_INSTANCE_ID env override) — the e2e suite compares it against its own checkout before running, so it never tests a sibling fork that happens to hold the same port.
Current Route Handler Surface
GET|POST /api/auth/[...nextauth] → Auth.js v5 (session, 工牌登入/登出)
GET /api/health → health probe
GET /api/openapi → OpenAPI 描述瑞成 PMS 沒有金流 webhook(Stripe / ECPay billing 功能已從產品移除),也沒有 OAuth callback — 登入採工牌編號 + 密碼的 Credentials provider。
Auth Session (Auth.js v5)
Auth.js v5 uses the JWT strategy — sessions are stateless. The session object is:
type Session = {
user: {
id: string
badge: string // 工牌編號,例如 "R00001"
name: string | null
role: 'admin' | 'operator' | 'staff' // 管理員 / 行政 / 一般(登入當下的有效角色快照)
}
expires: string
}session.user.role 只是登入當下的快照;授權一律以資料庫 users.role(有效角色)為準——guard 每次都 re-read。停用(離職)或已刪除的帳號沒有有效角色(getLiveRole 回傳 undefined,所有 guard 即時拒絕);lib/auth.ts 的 jwt callback 也會在每次請求檢查帳號是否仍有效(tokenAccountIsLive),否則回傳 null 結束 session(auth() 為 null、cookie 清除),因此舊 JWT 在停用當下就失效,不必等到期(E405);Edge proxy.ts 只用精簡 authConfig,仍只檢查 token 是否存在。E406 起登入可撤銷:users.session_version(migration 0030)+ JWT 的 sv 宣告;jwt callback 比對不符即回傳 null。管理員重設密碼、自己變更密碼、密碼重設連結、停用、CSV 匯入更新列(有填密碼)會 +1(bumpSessionVersion(),lib/session-version.ts);啟用與編輯不動。所以被停用又重新啟用的帳號,舊 cookie 不會復活。無 sv 宣告的舊 token 視為 0。登入效期 session.maxAge = 7 天(SESSION_MAX_AGE_SECONDS,新登入才適用)。E402 起一個帳號可持有多個角色(users.roles),users.role 恆為其中最高者,所以快照與 guard 的語意不變。
Read it server-side with auth() from @/lib/auth. The Credentials provider 以工牌編號 + 密碼 驗證,requires JWT sessions(it cannot create DrizzleAdapter database sessions)。
Server Actions
Server Actions ("use server") run on the server and are invoked directly from React components or forms. They compile to same-origin POST endpoints — they cannot be called cross-origin.
案件 (actions/cases.ts)
createCase(input): Promise<Result> // 4 步精靈建檔
editCase(id, input): Promise<Result>
setCaseHardDeadline(caseId, input): Promise<Result> // 只改硬期限(createCase 或此案被指派技師)
voidCase(id, reason): Promise<Result> // 軟刪除(作廢),必填原因,可復原
restoreCase(id): Promise<Result>
updateProgress(...): Promise<Result> // 自填式進度(必填備註;覆寫他人僅 admin)
completeNode(caseId, nodeId): Promise<Result>
addPrereq / editPrereq / markPrereq / deletePrereq(...) // 前置資料(NAS 連結、備註 note ≤200 字,trim、空→null)
addSubcase / editSubcase / subStatusChange / deleteSubcase(...) // B2 母子追加案
addNode / updateNodeContent(...) // 彈性混合式(B3)新增/改節點名稱、權重;已完成節點鎖定(E352)每個 mutation 依角色呼叫對應守衛(見下方 RBAC),驗證輸入,並經 rcLog 寫入稽核日誌。 進度 % 僅技師(admin)可覆寫他人 — 行政(operator)不可填進度(核心責任界線)。
複製節點內容(E354 → E400): 無新 Server Action — 列表/詳情頁的「複製節點內容」按鈕以
?duplicate=<caseId>深連結開啟既有的新增案件精靈,只預選來源分類並帶入來源案件目前的節點 (名稱/權重/交付物/Memo);業主、窗口、負責人員、協力廠商、前置資料與日期留白。送出時呼叫createCase(input)並帶選填的copyFromCaseId與nodes[]:伺服器確認來源可見、分類相同,驗證 1–20 節點與權重合計 100,再重新讀取來源節點比對(行政權重須與來源相同,僅管理員可調)。詳見 Server Actions 參考。
案件窗口(E360):
setCaseContacts(caseId, contactIds[])(actions/cases/contacts.ts)——案件 從業主資料庫選取窗口(M:N 表case_contacts,migration 0022 含 backfill),不再在案件頁直接 編輯共用聯絡人(E358 的 inline 編輯已移除)。權限比照editCase(createCase旗標 + 案件可見、 未作廢);每個 contactId 須屬於案件業主(跨業主/重複/不存在 → 拒絕),空陣列合法;伺服器算差集, 稽核記 before/after 窗口姓名。createCase另收選填contactIds(同樣規則,先驗證再寫入)。 聯絡人離職(toggleContactResign)不會自動移出案件,案件卡改標「已離職」。
v1.3.0 新增(Phases 99–103): 外審輪次
addReviewRound/editReviewRound/deleteReviewRound(actions/cases/review-rounds.ts,E391)、現場勘查行程createSiteVisit/editSiteVisit/setSiteVisitDone/deleteSiteVisit(actions/cases/site-visits.ts,E392)、公文回覆setOfficialDocReplied(actions/official-docs.ts,E390),以及createCase/editCase的hardDeadline/hardDeadlineKind欄位。E403 另有窄動作setCaseHardDeadline(被指派技師可設自己 案件的硬期限),外審輪次/現勘行程的授權放寬為「被指派技師、createCase或editNodes」(判定集中在lib/rc-case-actor-rules.ts)。授權、稽核與資料表細節見 Server Actions 參考。儀表板/我的待辦的改版沒有新增 Server Action—— 都是讀取層與純函式(lib/rc-dashboard-utils.ts、lib/rc-days-utils.ts),重點慣例: 「今天」一律在請求內呼叫getRcToday();畫面天數為日曆天、燈號為業主工作日 %。
業主 CRM (actions/clients.ts)、協力廠商 (actions/partners.ts)、待辦 (actions/todos.ts)、設定(accounts.ts / categories.ts / calendars.ts)
業主/窗口 CRUD、協力廠商 CRUD + CSV 匯出/匯入(exportPartnersCsv / importPartnersCsv,E351, 比照 E330 公文總表模式,詳見 Server Actions 參考)、業主資料庫 CSV 匯出/預覽/匯入 (exportClientsCsv / previewClientsCsv / importClientsCsv,E378,先預覽再確認)、自訂分類、業主工作日曆、 帳號(停用為軟刪除,不硬刪)。一般人員(staff)對 CRM 唯讀。
稽核與通知 (actions/admin.ts, actions/notifications*.ts)
稽核日誌僅 admin 可讀(可篩選 + 匯出 CSV,永久不可刪不可改);通知為系統內鈴鐺,不寄 Email,由 in-process 排程每小時整點掃描(lib/scheduler-worker.ts,NOTIFICATION_SCAN_CRON 可覆寫)。 儀表板「最近異動」對非 Admin 只顯示自己看得到的案件(含子案)動態,且不含登入事件(getCaseActivityForDisplay / getFirmActivityForDisplay,lib/audit.ts)。
RBAC Guard Pattern
Guards live in @/lib/permissions。每個守衛解析 session 並從資料庫 re-read live role (非 JWT 快照),因此降權立即生效。權限矩陣的唯一真實來源是 lib/rc-permissions.ts(RC_PERM_MATRIX,10 旗標 × 三角色)。
| Guard | Allows |
|---|---|
requireAuth() | 任何已登入使用者 |
requireOperatorOrAdmin() | admin、operator(文書類操作) |
requireAdmin() | admin(樣板/分類管理、稽核日誌、進度覆寫) |
requireFlag(flag) | 具備指定權限旗標的角色(依 RC_PERM_MATRIX) |
import { requireAdmin } from '@/lib/permissions'
export async function adminAction() {
'use server'
const session = await requireAdmin() // re-reads role from DB; redirects non-admins
// ...admin logic
}Server Actions are public POST endpoints — never trust an argument's TypeScript type at runtime. Validate against a shared Zod schema (lib/validations/*) or a runtime allowlist。
Validation Envelope
Item/account actions return a small state object — { error?: string } | null (null = success). Admin actions return { success: true } | { error: string }. Validation schemas are shared from lib/validations/* so the same Zod rules run on the client form and the server action.
資料庫關聯 Schema
全部資料表的 ER 圖(Mermaid)、FK 基數、ON DELETE 行為與對應 migration 見 db-schema-relationships.md (scripts/checks/check-schema-doc.sh 會驗證文件涵蓋 lib/schema/*.ts 的每張表)。
Interactive Playground
Test live Route Handlers against your deployed Next.js app: