From 7cece36062349bfd906c8b253b5560f96588556d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=B3=E6=AC=A3=E6=80=A1?= Date: Wed, 10 Jun 2026 10:22:17 +0800 Subject: [PATCH 1/3] docs: bump foundation-000 and account-001 plans to v2.0.0, sync STATUS.md Co-Authored-By: Claude Opus 4.7 --- specs/STATUS.md | 4 +- .../account/001-login-email-password/plan.md | 518 ++++++++++-------- specs/foundation/000-foundation/plan.md | 150 +++-- 3 files changed, 394 insertions(+), 278 deletions(-) diff --git a/specs/STATUS.md b/specs/STATUS.md index be0de619..8adae260 100644 --- a/specs/STATUS.md +++ b/specs/STATUS.md @@ -23,7 +23,7 @@ | ID | 功能 | 模組 | 狀態 | 分支 | 備註 | | --- | --- | --- | --- | --- | --- | -| foundation-000 | Foundation — 工程基準與共同約束 | foundation | `plan-ready` | `feat/foundation/000-foundation` | spec v1.12.2;FR-001~131;SC-001~045;plan v1.0.3(Foundation-Core);Observability 延後 | +| foundation-000 | Foundation — 工程基準與共同約束 | foundation | `plan-ready` | `feat/foundation/000-foundation` | spec v1.12.2;FR-001~131;SC-001~045;plan v2.0.0(Foundation-Core);Observability 延後 | | account-001 | Login — Email / Password | account | `plan-ready` | `feat/account/001-login-email-password` | spec v1.2.2;規格狀態:Clarified | | account-002 | Login — Google SSO | account | `spec-ready` | `feat/account/002-login-google-sso` | spec v1.2.2;規格狀態:Clarified | | account-003 | Register — Email / Password | account | `spec-ready` | `feat/account/003-register-email-password` | spec v1.2.3;規格狀態:Clarified | @@ -55,6 +55,8 @@ | 日期 | 更新內容 | |------|----------| +| 2026-06-09 | Update account-001 status to `plan-ready`. | +| 2026-06-09 | Update foundation-000 status to `plan-ready`. | | 2026-06-05 | foundation-000 plan v1.0.0 created (Foundation-Core): plan-ready; scope F-01~F-10, F-13, F-16, F-18; Observability/Celery deferred; health check endpoint added. | | 2026-06-04 | Update foundation-000 to spec v1.12.0: pagination switched from page/page_size to limit/offset; PaginatedResponse next_offset added. | | 2026-06-03 | Update foundation-000 to spec v1.11.5: SC-045 naming change applied. | diff --git a/specs/account/001-login-email-password/plan.md b/specs/account/001-login-email-password/plan.md index 2135bf28..426dc535 100644 --- a/specs/account/001-login-email-password/plan.md +++ b/specs/account/001-login-email-password/plan.md @@ -1,73 +1,64 @@ --- 功能分支: feat/account/001-login-email-password 建立日期: 2026-05-28 -版本: 1.0.0 -狀態: Draft +版本: 2.0.0 +狀態: plan-ready --- # 實作計畫:登入 — Email / Password + 頁面 UI **規格**: [specs/account/001-login-email-password/spec.md](spec.md) -**輸入**: `specs/account/001-login-email-password/spec.md` -## 執行流程(/speckit.plan 範圍) +## 功能目標 -```text -1. 從輸入路徑載入功能規格 - → 若未找到:ERROR "No feature spec at {path}" -2. 填寫技術脈絡 -3. 評估下方憲章檢查 - → 若存在違反項目:記錄至複雜度追蹤 - → 若無正當理由:ERROR "Simplify approach first" -4. 執行 Phase 0 → 研究(若有未知事項) - → 若仍有 NEEDS CLARIFICATION:ERROR "Resolve unknowns before proceeding" -5. 執行 Phase 1 → 契約、資料模型、系統流程 -6. 重新評估憲章檢查 - → 若發現新違反:重構設計,返回 Phase 1 -7. 描述任務產生方式(不得建立 tasks.md) -8. 停止 — 準備好進入 /speckit.tasks -``` +> 摘自 spec.md v1.2.2 -**重要**:/speckit.plan 在第 7 步停止。任務建立由 /speckit.tasks 負責。 +使用者能透過 Email / Password 登入 Label Suite,並從登入頁導向 dashboard,或前往註冊、忘記密碼頁面。 ---- +完整行為: -## 摘要 +- 未登入使用者開啟 `/login`,見到完整登入表單(導覽列 + Google 按鈕 + Email/Password 欄位 + 導流連結)。 +- 填妥 Email/Password 送出後,後端驗證憑證,成功回傳 JWT;前端存入 `authStore`(Zustand + localStorage),導向 `/dashboard`。 +- Email/Password 任一缺漏時顯示欄位錯誤;憑證錯誤(401)或帳號停用(403)顯示 inline error banner。 +- 頁面支援 zh/en 雙語切換,語言狀態透過 `localStorage['labelsuite.lang']` 持久化並跨頁維持。 +- RWD:支援 375px / 768px / 1440px 三種視口,`MOBILE_BP = 767px`。 -登入頁(Email / Password)是 Label Suite 的第一個入口點,涵蓋: +**Spec 擴展說明**:spec.md v1.2.2 將 JWT 與後端 API 列為「不在本版範圍」(prototype 確認階段)。本 plan 擴展至真實實作範圍,補充後端 API 設計與 token 管理。 -- **後端**:`POST /api/v1/auth/login`,以 bcrypt 驗證密碼後簽發 JWT access token;`GET /api/v1/auth/me` 取得目前用戶資訊;依賴 `User` 模型。 -- **前端**:LoginPage 呼叫真實 API,成功後將 JWT 存入 Zustand authStore + localStorage,導向 `/dashboard`;失敗顯示 inline 錯誤訊息。 -- **UI**:對齊 prototype [design/prototype/pages/account/login.html](../../../design/prototype/pages/account/login.html) — 表單驗證、密碼顯示切換、zh/en 雙語(localStorage 持久化)、RWD(MOBILE_BP = 767px)。 +## 技術方向 -> **Spec 擴展說明**:spec.md v1.2.2 將 JWT 與後端 API 列為「不在本版範圍」(prototype 確認階段)。本 plan 擴展至真實實作範圍,補充後端 API 設計與 token 管理。Spec 無需更新版本;下游規格(002 Google SSO、spec 004 Forgot/Reset Password 中的 token 失效邏輯)保持依賴關係不變。 - ---- +本功能同時觸及後端與前端。後端採 FastAPI module-first 架構(`app/modules/auth/`),以 `passlib[bcrypt]` 驗證密碼後簽發 JWT(`app/core/security.py`);前端為 React 18 + Vite vertical-slice 架構(`src/features/account/`),`LoginPage` 呼叫真實 API,成功後將 JWT 存入 Zustand `authStore` + localStorage 並導向 `/dashboard`。失敗路徑(401/403)顯示 inline error banner,語言與 token 狀態透過 shared hooks/stores 跨頁持久化。 ## 技術脈絡 **語言 / 版本**: Python 3.12+ / TypeScript 5+ -**主要相依套件**: FastAPI / React 18 + Vite 5 / react-router-dom v6 / react-i18next / TanStack Query v5 / Zustand v5 -**儲存**: PostgreSQL(User table)/ 無 Redis(此 feature 無需 refresh token blacklist) -**測試**: pytest + pytest-asyncio / Vitest + Testing Library / Playwright + Storybook -**目標平台**: Web(瀏覽器 + REST API) -**效能目標**: `POST /auth/login` P95 < 500ms(密碼 hash 為主要瓶頸,bcrypt cost factor 12 在現代硬體約 100ms) -**限制**: 不涉及 task type 邏輯,不影響 config-driven architecture;JWT secret 必須從環境變數讀取 +**主要相依套件**: FastAPI / React 18 + Vite 5 / react-router-dom v6 / react-i18next / TanStack Query v5 / Zustand v5 / shadcn/ui / passlib[bcrypt] / python-jose[cryptography] +**儲存**: PostgreSQL(prod)/ SQLite(local dev,ADR-024)/ 無 Redis(無需 token blacklist) +**測試**: pytest + pytest-asyncio / Vitest + Testing Library + MSW / Playwright +**效能目標**: `POST /api/v1/auth/login` P95 < 500ms(bcrypt cost=12 約 100ms) +**限制**: 不涉及 task type 邏輯;JWT secret 從環境變數讀取;HttpOnly cookie defer 至 security spec;i18n 邊界:前端 locale 檔僅含 UI 字串,後端 `detail` 依 `Accept-Language` 回傳 ---- +**前置條件**: 本 feature 依賴 Foundation-Core(`specs/foundation/000-foundation`)骨架已實作:`app/core/`、`app/api/v1/router.py`、`AppBaseModel`、`ErrorResponse`、`PaginatedResponse`、`conftest.py`、Vite + React 環境、`shared/api/apiClient.ts`、Docker Compose 服務。Foundation tasks.md 必須先完成後,方可開始本 feature 實作。 ## 憲章檢查 +- [x] 功能目標:本計畫的功能目標與 spec.md 一致(plan 擴展後端範圍已說明) - [x] I. Spec-First:spec.md 狀態 Clarified v1.2.2;plan 擴展範圍已說明 - [x] II. Generalization-First:登入邏輯不涉及 NLP task type,無 hardcoded task logic - [x] III. Data Fairness:無 test set 或 ground truth 資料 - [x] IV. Test-First:測試計畫已列於 Phase 1 步驟 5;TDD 順序寫入 Phase 2 -- [x] V. Code Quality & Simplicity:controlled components(2 欄位,簡單驗證);無 react-hook-form 過度工程;bcrypt 為標準做法 +- [x] V. Code Quality & Simplicity:controlled components(2 欄位,簡單驗證);bcrypt 業界標準;入口點 LoginPage → LoginForm → submit → auth.ts → POST /auth/login,兩層內可定位 - [x] VI. English-First:程式碼/commit 用英文;specs/prototype 允許繁體中文 -- [x] VII. Design Consistency:前端 token 對齊 `design/system/MASTER.md`;prototype 為 UI source of truth -- [x] VIII. Performance Baseline:登入無列表端點;API P95 目標已設定;無 N+1 風險 +- [x] VII. Design Consistency:UI 對齊 `design/prototype/pages/account/login.html`;shadcn/ui + MASTER.md tokens;非 page 元件均規劃 Storybook story;符合 WCAG 2.1 AA(keyboard navigable、`aria-describedby` on inputs、eye toggle aria-label 同步) +- [x] VIII. Performance Baseline:`POST /auth/login` P95 < 500ms;前端 FCP ≤ 3s;互動反饋 ≤ 100ms;`LoginPage` 使用 route-level lazy loading +- [x] IX. No Silent Failure:全 error case 定義(401/403/422/5xx);loading overlay 防重複提交;API error 顯示 inline banner;`catch` 不靜默吞噬 +- [x] XI. Security & Privacy Baseline:密碼 bcrypt hash;JWT secret 從 env;login 失敗統一 401(防 user enumeration);`hashed_password` 不出現於任何 response schema;CORS 明確 origins(Foundation 已設定) ---- +### 領域憲章載入 + +- [x] 後端(touches `backend/`):已讀取 `.specify/memory/backend-constitution.md`;本 plan 符合其所有適用規則 +- [x] 前端(touches `frontend/`):已讀取 `.specify/memory/frontend-constitution.md`;本 plan 符合其所有適用規則 +- [x] 測試(所有 task):已讀取 `.specify/memory/testing-constitution.md`;本 plan 符合其所有適用規則 ## 專案結構 @@ -78,75 +69,96 @@ specs/account/001-login-email-password/ ├── spec.md ├── plan.md ├── tasks.md -├── data-model.md -├── contracts/ -│ └── auth-login.md -└── checklists/ - ├── ac-checklist.md - └── security-checklist.md +├── checklists/ +│ ├── ac-checklist.md +│ └── security-checklist.md +└── contracts/ + └── auth-login.md ``` ### 原始碼 ```text -backend/ -├── app/ -│ ├── api/ -│ │ └── routes/ -│ │ └── auth.py # login + me endpoints -│ ├── core/ -│ │ ├── auth.py # JWT sign/verify, password hash/verify -│ │ ├── config.py # JWT_SECRET, ALGORITHM, TOKEN_EXPIRE_MINUTES -│ │ └── deps.py # get_current_user dependency -│ ├── models/ -│ │ └── user.py # User SQLAlchemy model -│ └── schemas/ -│ ├── auth.py # LoginRequest, TokenResponse -│ └── user.py # UserBase, UserResponse -└── tests/ - ├── unit/ - │ └── test_auth_core.py # hash/verify, token create/decode - └── integration/ - └── test_auth_routes.py # POST /login, GET /me - frontend/ ├── src/ │ ├── features/ │ │ └── account/ │ │ ├── components/ │ │ │ └── login/ +│ │ │ ├── AccountNavbar.tsx +│ │ │ ├── AccountNavbar.stories.tsx │ │ │ ├── LoginCard.tsx +│ │ │ ├── LoginCard.stories.tsx │ │ │ ├── LoginForm.tsx +│ │ │ ├── LoginForm.stories.tsx │ │ │ ├── PasswordField.tsx +│ │ │ ├── PasswordField.stories.tsx │ │ │ ├── GoogleLoginButton.tsx -│ │ │ └── AccountNavbar.tsx # account pages only, stays in account/ +│ │ │ └── GoogleLoginButton.stories.tsx │ │ ├── pages/ │ │ │ └── LoginPage.tsx │ │ ├── services/ -│ │ │ └── auth.ts # API call: POST /api/v1/auth/login +│ │ │ └── auth.ts # POST /api/v1/auth/login wrapper │ │ ├── types/ │ │ │ └── auth.ts # LoginFormState, AuthResponse, UserInfo │ │ └── __tests__/ │ │ ├── LoginForm.test.tsx │ │ └── LoginPage.test.tsx -│ └── shared/ -│ ├── components/ -│ │ └── LanguageToggle.tsx # 2+ modules: account + dashboard+ -│ ├── hooks/ -│ │ └── useLanguage.ts # localStorage-backed, cross-module -│ └── stores/ -│ └── authStore.ts # Zustand: {token, user, setAuth, clearAuth} -├── locales/ -│ ├── zh-TW/ -│ │ └── account.json -│ └── en/ -│ └── account.json +│ ├── shared/ # Foundation 建立骨架(components/, hooks/, stores/, constants/, types/, api-types/, utils/, styles/, i18n/) +│ │ ├── components/ +│ │ │ ├── LanguageToggle.tsx # 2+ modules → shared/ +│ │ │ └── LanguageToggle.stories.tsx +│ │ ├── hooks/ +│ │ │ └── useLanguage.ts # localStorage-backed,全站語言 hook +│ │ ├── stores/ +│ │ │ └── authStore.ts # Zustand: {token, user, setAuth, clearAuth} +│ │ └── __tests__/ +│ │ └── useLanguage.test.ts +│ └── locales/ +│ ├── zh-TW/ +│ │ └── account.json +│ └── en/ +│ └── account.json └── e2e/ └── account/ └── login.spec.ts + +backend/ +├── app/ +│ ├── core/ # Foundation 已建立(config.py, schemas.py, errors.py) +│ │ ├── security.py # bcrypt hash/verify, JWT create/decode +│ │ └── deps.py # get_current_user dependency(Foundation 骨架 or auth task) +│ ├── api/ +│ │ └── v1/ +│ │ └── router.py # Foundation 已建立;須 include auth router +│ └── modules/ +│ └── auth/ +│ ├── router.py # login + me endpoints +│ ├── service.py # authenticate_user +│ ├── repository.py # get_user_by_email, get_user_by_id +│ ├── models.py # User SQLAlchemy model +│ ├── schemas.py # LoginRequest, TokenResponse, UserBase, UserResponse, UserRole +│ ├── dependencies.py # re-export get_current_user from core/deps.py +│ ├── constants.py # ACCESS_TOKEN_EXPIRE_MINUTES default +│ └── exceptions.py # AuthError helpers(薄包裝 HTTPException) +├── alembic/ +│ └── versions/ +│ └── [hash]_create_users_table.py +├── tests/ +│ ├── conftest.py # Foundation 已建立;補充 auth fixtures +│ ├── factories/ +│ │ └── user_factory.py +│ └── auth/ +│ ├── test_auth_core.py # unit: hash/JWT +│ └── test_auth_routes.py # integration: POST /login, GET /me +└── bruno/ + └── account/ + └── 001-login-email-password/ + ├── post-auth-login.bru + └── get-auth-me.bru ``` ---- +> **拆分慣例**:auth module 各檔案預計均低於 300 行,維持單一檔案。若未來擴展超過 300 行,依 plan-template 拆分慣例改為同名子目錄,`__init__.py` 負責彙總對外介面。 ## 系統流程與資料流 @@ -156,38 +168,48 @@ sequenceDiagram participant LoginPage participant AuthService as auth.ts (frontend) participant AuthStore as authStore (Zustand) - participant API as POST /api/v1/auth/login - participant BackendService as auth_service.py + participant Route as app/api/v1/router.py + participant Controller as app/modules/auth/router.py + participant Service as auth/service.py + participant Repository as auth/repository.py + participant Model as User (SQLAlchemy) participant DB as users table User->>LoginPage: submit {email, password} LoginPage->>LoginPage: trim email, validate non-empty alt validation fails - LoginPage-->>User: field-level error (emailRequired / passwordRequired) + LoginPage-->>User: field-level error (email_required / password_required) else passes - LoginPage->>LoginPage: isSubmitting = true (disabled + spinner, page overlay) + LoginPage->>LoginPage: isSubmitting = true (disabled + spinner + overlay) LoginPage->>AuthService: login({email, password}) - AuthService->>API: POST /api/v1/auth/login {email, password} - API->>BackendService: authenticate_user(email, password) - BackendService->>DB: SELECT * FROM users WHERE email = ? - DB-->>BackendService: User | None + AuthService->>Route: POST /api/v1/auth/login {email, password} + Route->>Controller: dispatch to auth router + Controller->>Controller: Pydantic validate LoginRequest + Controller->>Service: authenticate_user(email, password, db) + Service->>Repository: get_user_by_email(db, email) + Repository->>Model: SELECT * FROM users WHERE email = ? + Model->>DB: query + DB-->>Model: User | None + Model-->>Repository: User | None + Repository-->>Service: User | None alt user not found or password mismatch - BackendService-->>API: raise HTTP 401 - API-->>AuthService: 401 {detail: "Invalid credentials"} - AuthService-->>LoginPage: throw AuthError + Service-->>Controller: raise HTTP 401 + Controller-->>AuthService: 401 {detail: "帳號或密碼錯誤,請再試一次"} + AuthService-->>LoginPage: throw AuthError(401) LoginPage->>LoginPage: isSubmitting = false - LoginPage-->>User: show inline error banner (i18n key: login.invalid_credentials) - else inactive account - BackendService-->>API: raise HTTP 403 - API-->>AuthService: 403 {detail: "Account disabled"} - AuthService-->>LoginPage: throw AuthError - LoginPage-->>User: show inline error banner + LoginPage-->>User: inline error banner (detail from response) + else account disabled + Service-->>Controller: raise HTTP 403 + Controller-->>AuthService: 403 {detail: "此帳號已被停用,請聯繫管理員"} + AuthService-->>LoginPage: throw AuthError(403) + LoginPage-->>User: inline error banner (detail from response) else credentials valid + account active - BackendService->>BackendService: create_access_token({sub: user.id, role: user.role}) - BackendService-->>API: TokenResponse {access_token, token_type, user} - API-->>AuthService: 200 TokenResponse + Service->>Service: create_access_token({sub: user.id, role: user.role}) + Service-->>Controller: TokenResponse {access_token, token_type, user} + Controller-->>Route: 200 TokenResponse + Route-->>AuthService: 200 TokenResponse AuthService-->>LoginPage: TokenResponse LoginPage->>AuthStore: setAuth(access_token, user) AuthStore->>localStorage: write labelsuite.token @@ -198,43 +220,45 @@ sequenceDiagram | 層 | 元件 | 職責 | |----|------|------| -| Frontend Page | `LoginPage` | 表單狀態、語言 init、API 呼叫、錯誤顯示、導頁 | +| Frontend Page | `LoginPage` | 表單狀態、語言 init、mutation、錯誤顯示、導頁 | | Frontend Service | `auth.ts` | fetch wrapper,回傳 `TokenResponse` 或拋出 `AuthError` | | Frontend Store | `authStore` | Zustand: 存取 token + user;同步至 localStorage | -| API | `routes/auth.py` | 請求驗證(Pydantic),委派 auth_service,回傳 TokenResponse | -| Service | `auth_service.py` | 查找 user、驗證密碼、簽發 JWT | -| DB | `models/user.py` | 持久化 User 實體 | +| Route | `app/api/v1/router.py` | API v1 路由彙整(Foundation 已建立) | +| Controller boundary | `app/modules/auth/router.py` | 請求驗證(Pydantic)、委派 service、包裝 HTTP response | +| Service | `app/modules/auth/service.py` | 查找 user、驗證密碼、簽發 JWT | +| Repository | `app/modules/auth/repository.py` | DB 查詢:get_user_by_email、get_user_by_id | +| Model | `app/modules/auth/models.py` | User SQLAlchemy ORM 定義 | +| DB | `users` table | 持久化 | --- ## Phase 0:研究 -> Spec 狀態 Clarified,所有 UI 問題已解答。後端技術選型如下,無 NEEDS CLARIFICATION: +> Spec 狀態 Clarified,所有 UI 問題已解答;後端技術選型完整,無 NEEDS CLARIFICATION。 **技術決策:** | 決策項目 | 選擇 | 原因 | |---------|------|------| -| 密碼 hash | `passlib[bcrypt]` with cost=12 | 業界標準;passlib 已是 FastAPI 生態推薦 | -| JWT | `python-jose[cryptography]` | FastAPI 官方文件採用;支援 HS256 | -| Token 儲存(前端) | Zustand store + `localStorage` | localStorage 持久化,app reload 後維持登入;XSS 風險可接受(本階段無 HttpOnly cookie,defer 至安全加固 spec) | -| Access token 有效期 | 30 分鐘(env: `ACCESS_TOKEN_EXPIRE_MINUTES`) | 平衡安全性與 UX;refresh token 機制 defer 至 002 後 | -| i18n library | `react-i18next` | 符合現有 namespace 命名規範;成熟的 SSR/lazy 支援 | -| 路由守衛策略 | `PrivateRoute` wrapper:未登入 → `/login` | 簡單 HOC,後續可擴展 role-based guard | +| 密碼 hash | `passlib[bcrypt]` cost=12 | 業界標準;FastAPI 生態推薦;支援 future hash upgrade | +| JWT | `python-jose[cryptography]` | FastAPI 官方文件採用;支援 HS256;足夠輕量 | +| Token 儲存(前端) | Zustand store + `localStorage` | 跨 session 持久化;XSS 風險已記錄於複雜度追蹤 | +| Access token 有效期 | 30 分鐘(env: `ACCESS_TOKEN_EXPIRE_MINUTES`) | 平衡安全性與 UX;refresh token defer | +| i18n library | `react-i18next` | 符合 Foundation 建立的 namespace 規範;成熟的 lazy 支援 | +| 路由守衛策略 | `PrivateRoute` wrapper:未登入 → `/login?redirect_to=...` | 簡單 HOC,後續可擴展 role-based guard | +| 後端模組 | `app/modules/auth/` | 對齊 Foundation module-first 架構 | **Exception 設計:** | 操作 | Error 情境 | Exception Class | HTTP Status | Response body | |------|-----------|----------------|-------------|---------------| -| `POST /auth/login` | email 不存在 / 密碼錯誤 | `HTTPException` | 401 | `{detail: "Invalid credentials"}` | -| `POST /auth/login` | 帳號停用 (`is_active=False`) | `HTTPException` | 403 | `{detail: "Account disabled"}` | -| `GET /auth/me` | token 過期 | `HTTPException` | 401 | `{detail: "Token expired"}` | -| `GET /auth/me` | token 無效 | `HTTPException` | 401 | `{detail: "Invalid token"}` | +| `POST /auth/login` | email 不存在 / 密碼錯誤 | `HTTPException` | 401 | `{detail: i18n("auth.invalid_credentials")}` | +| `POST /auth/login` | 帳號停用 (`is_active=False`) | `HTTPException` | 403 | `{detail: i18n("auth.account_disabled")}` | +| `GET /auth/me` | token 過期 | `HTTPException` | 401 | `{detail: i18n("auth.token_expired")}` | +| `GET /auth/me` | token 無效 / 缺少 | `HTTPException` | 401 | `{detail: i18n("auth.token_invalid")}` | > 注意:login 失敗一律回 401(不區分「email 不存在」vs「密碼錯誤」),防止 user enumeration 攻擊。 -**產出**:無需建立 research.md(已無 NEEDS CLARIFICATION) - --- ## Phase 1:設計與契約 @@ -245,44 +269,51 @@ sequenceDiagram | 欄位 | 型別 | 說明 | |------|------|------| -| `id` | `UUID` PK | 主鍵 | +| `id` | `UUID` PK | 主鍵(server-side generated) | | `email` | `String(254)` UNIQUE NOT NULL | 登入識別 | -| `hashed_password` | `String` NOT NULL | bcrypt hash | -| `role` | `Enum('user', 'super_admin')` DEFAULT 'user' | 系統角色 | -| `is_active` | `Boolean` DEFAULT True | 帳號啟用狀態 | -| `created_at` | `DateTime` | 建立時間(auto) | -| `updated_at` | `DateTime` | 更新時間(auto) | +| `hashed_password` | `String` NOT NULL | bcrypt hash;不得出現於任何 response schema | +| `role` | `Enum('user', 'super_admin')` DEFAULT `'user'` | 系統角色 | +| `is_active` | `Boolean` DEFAULT `True` | 帳號啟用狀態 | +| `created_at` | `DateTime(timezone=True)` | 建立時間(auto utcnow) | +| `updated_at` | `DateTime(timezone=True)` | 更新時間(auto utcnow onupdate) | + +**狀態轉換**:本功能無多狀態實體。User `is_active` 僅 True/False,由 admin-006 管理,不在本 spec 範圍。 **DB Index 分析**: | 查詢 | 篩選欄位 | Index 策略 | Loading Strategy | 風險 | |------|---------|-----------|-----------------|------| | 登入查詢 | `email` | `UNIQUE INDEX users(email)` | 直接查詢,無 relationship | — | -| JWT 驗證 | `id` | Primary key (UUID) | 直接查詢,無 relationship | — | +| JWT 驗證(GET /me) | `id` | Primary key(UUID) | 直接查詢,無 relationship | — | -> `lazy="raise"` 設於所有 relationship(此 model 目前無 relationship),防止未來新增欄位後產生隱性 N+1。 +> `lazy="raise"` 設於所有 relationship(本 model 目前無 relationship),防止未來新增欄位後產生隱性 N+1。 --- ### 2. 後端 API 清單 -| Method | Path | System Role | Task Role | Auth Dependency | 說明 | -|--------|------|-------------|-----------|----------------|------| -| POST | `/api/v1/auth/login` | 無(公開) | 無 | 無 | Email/password 驗證,回傳 JWT | -| GET | `/api/v1/auth/me` | user / super_admin | 無 | `get_current_user` | 取得目前登入用戶資訊 | +| Method | Path | System Role | Task Role | Auth Dependency | 說明 | Bruno 檔案 | +|--------|------|-------------|-----------|----------------|------|-----------| +| POST | `/api/v1/auth/login` | 無(公開) | 無 | 無 | Email/password 驗證,回傳 JWT | `backend/bruno/account/001-login-email-password/post-auth-login.bru` | +| GET | `/api/v1/auth/me` | user / super_admin | 無 | `get_current_user` | 取得目前登入用戶資訊 | `backend/bruno/account/001-login-email-password/get-auth-me.bru` | 完整契約 → `contracts/auth-login.md` +**事務邊界設計**:兩個端點均為單一 DB 讀取,無複合寫入操作。本端點無複合事務。 + --- ### 2b. Pydantic Schema 層次設計 | Schema | 繼承自 | 用途 | 需排除的敏感欄位 | |--------|-------|------|----------------| -| `LoginRequest` | `BaseModel` | POST /auth/login body:`email: EmailStr`、`password: str` | — | -| `TokenResponse` | `BaseModel` | 登入成功回應:`access_token: str`、`token_type: str = "bearer"`、`user: UserResponse` | `hashed_password` | -| `UserBase` | `BaseModel` | 共用欄位:`id: UUID`、`email: EmailStr`、`role: UserRole`、`is_active: bool` | — | -| `UserResponse` | `UserBase` | API 回應(含 `created_at`) | `hashed_password` | +| `UserRole` | `str, Enum` | 角色枚舉:`'user'`, `'super_admin'` | — | +| `LoginRequest` | `AppBaseModel` | POST /auth/login body:`email: EmailStr`、`password: str (min_length=1)` | — | +| `UserBase` | `AppBaseModel` | 共用欄位:`id: UUID`、`email: EmailStr`、`role: UserRole`、`is_active: bool` | — | +| `UserResponse` | `UserBase` | API 回應(含 `created_at: datetime`) | `hashed_password` | +| `TokenResponse` | `AppBaseModel` | 登入成功回應:`access_token: str`、`token_type: str = "bearer"`、`user: UserResponse` | `hashed_password`(透過 UserResponse 排除) | + +> `AppBaseModel` 繼承自 Foundation 建立的 `app/core/schemas.py`(設有 `model_config = ConfigDict(from_attributes=True)`)。 --- @@ -290,13 +321,12 @@ sequenceDiagram | 區塊 | 元件名稱 | 職責 | 資料來源 | Stories 狀態 | ARIA / 鍵盤需求 | 響應式行為 | |------|---------|------|---------|------------|----------------|----------| -| 頁面容器 | `LoginPage` | 路由入口、i18n init、mutation、導頁 | `useMutation`、`authStore` | — (page 層不寫 story) | — | — | -| 導覽列 | `AccountNavbar` | 品牌 + 語言切換(account 頁共用) | `useLanguage` | Default | `role="banner"` | height: 64px → 56px | -| 語言切換 | `LanguageToggle` | zh/en 切換,寫 localStorage | `useLanguage` | ZH, EN | `aria-label="切換語言"` | 不變 | -| 登入卡片 | `LoginCard` | 卡片容器 + header | props | Default | `role="region" aria-label` | padding 縮小 | -| 登入表單 | `LoginForm` | 2 欄位、驗證、submit | `useState`(controlled) | Default, Loading, EmailError, PasswordError, BothErrors, APIError | `novalidate`,`aria-describedby` on inputs | 全寬 | -| Email 欄位 | 內嵌於 `LoginForm` | email input + error span | controlled | — | `aria-describedby="emailError"` | — | -| Password 欄位 | `PasswordField` | password input + eye toggle + error | controlled | Default, Visible, Hidden, WithError | `aria-label` 切換(眼睛按鈕) | — | +| 頁面容器 | `LoginPage` | 路由入口、語言 init、mutation、導頁 | `useMutation`、`authStore` | — (page 層不寫 story) | — | — | +| 導覽列 | `AccountNavbar` | 品牌 Logo + 語言切換(account 頁共用) | `useLanguage` | Default | `role="banner"` | height: 64px → 56px at ≤767px | +| 語言切換 | `LanguageToggle` | zh/en 切換,寫 localStorage | `useLanguage` | ZH, EN | `aria-label` 隨語言切換 | 不變 | +| 登入卡片 | `LoginCard` | 卡片容器 + header(logo/title/subtitle)+ 底部導流 | props | Default | `role="region" aria-label="登入"` | padding 縮小 at ≤767px | +| 登入表單 | `LoginForm` | 2 欄位、欄位驗證、submit、API error banner | `useState`(controlled) | Default, Loading, EmailError, PasswordError, BothErrors, APIError | `novalidate`、`aria-describedby` on inputs | 全寬 | +| Password 欄位 | `PasswordField` | password input + eye toggle + error span | controlled | Default, Visible, Hidden, WithError | eye button `aria-label` 隨狀態切換 | — | | Google 按鈕 | `GoogleLoginButton` | no-op prototype(UI only) | — | Default, Hover | `aria-label="使用 Google 帳號繼續登入"` | 全寬 | **元件層次**: @@ -306,68 +336,85 @@ LoginPage ├── AccountNavbar │ └── LanguageToggle (shared/) └── LoginCard - ├── CardHeader (logo + title + subtitle — 內嵌於 LoginCard) + ├── CardHeader (內嵌:logo + title + subtitle) ├── GoogleLoginButton - ├── DividerWithText (內嵌於 LoginCard) + ├── DividerWithText (內嵌) ├── LoginForm - │ ├── EmailField (內嵌於 LoginForm) + │ ├── EmailField (內嵌) │ ├── PasswordField - │ └── LoginButton (內嵌於 LoginForm) - └── RegisterPrompt (內嵌於 LoginCard) + │ └── LoginButton (內嵌) + └── RegisterPrompt (內嵌) ``` **shared/ 資格判斷**: -- `LanguageToggle` → 2+ modules(account + dashboard/task-management 等)→ **shared/components/** +- `LanguageToggle` → account + dashboard+ 等 2+ modules → **shared/components/** - `useLanguage` → 全站語言 hook → **shared/hooks/** - `authStore` → 全站 auth 狀態 → **shared/stores/** - `AccountNavbar` → 僅 account module 內部(login/register/forgot-pw)→ **features/account/components/** +**畫面狀態轉換**: + +| 當前畫面狀態 | 觸發條件 | 下一狀態 | UI 呈現 | +|------------|---------|---------|--------| +| LoginForm Default | 提交空白欄位 | LoginForm FieldError | 欄位紅框 + error span 顯示 | +| LoginForm Default | 填妥欄位送出 | LoginForm Loading | button disabled + spinner + 全頁 overlay(pointer-events: none) | +| LoginForm Loading | API 401 / 403 | LoginForm APIError | overlay 移除 + button 恢復 + inline error banner(顯示後端 `detail`) | +| LoginForm Loading | API 200 | 導向 /dashboard | navigate('/dashboard') | +| LoginForm FieldError | 使用者重新輸入 | 對應欄位清除錯誤 | 單欄位即時清除,不影響其他欄位 | + +**畫面 × API 對應**(必填): + +| 畫面 / 元件 | 觸發時機 | Method | Endpoint | TanStack Query key | +|------------|---------|--------|----------|--------------------| +| `LoginPage` 掛載 | 頁面初始化(已登入 redirect check) | GET | `/api/v1/auth/me` | `QUERY_KEYS.auth.me` = `['auth', 'me']` | +| `LoginForm` 送出 | 使用者操作 | POST | `/api/v1/auth/login` | — (mutation,onSuccess invalidates `QUERY_KEYS.auth.me`) | + +> `QUERY_KEYS` 集中宣告於 `src/shared/constants/queryKeys.ts`(Foundation 已建立此常數檔結構)。 + **前端技術決策**: ```text -型別策略: +型別策略(擇一): - [x] 手寫 interface(src/features/account/types/auth.ts) - 原因:無 OpenAPI spec 可生成;型別少且穩定 + 原因:無 OpenAPI spec 可生成;型別數量少且穩定 -表單策略: -- [x] controlled component(2 欄位:email + password) - 原因:僅 2 欄位,驗證邏輯為 non-empty + trim;react-hook-form 屬過度工程 +表單策略(擇一): +- [x] controlled component(欄位 ≤ 3 個的簡單表單) + 原因:僅 2 欄位(email + password);驗證為 non-empty + trim;react-hook-form 屬過度工程 TanStack Query 策略: -- queryKey:['auth', 'me'](GET /auth/me 快取) -- mutation:useMutation for POST /auth/login(無 queryKey) -- mutationFn 成功 → invalidate ['auth', 'me'] +- queryKey 格式:QUERY_KEYS.auth.me = ['auth', 'me'](從 shared/constants/queryKeys.ts 匯入) +- mutation:useMutation for POST /auth/login → onSuccess: setAuth(token, user) + invalidate QUERY_KEYS.auth.me - 無 optimistic update(登入操作為 all-or-nothing) API 錯誤處理策略: -- 401/403 server error → inline error banner(matches prototype pattern,不用 toast) -- 422 validation error(Pydantic)→ 一般不出現(前端已驗證)→ Error Boundary fallback -- 5xx → Error Boundary(頁面層) - -Loading 策略: -- 登入提交中 → button disabled + spinner + 全頁 pointer-events: none overlay(spec FR-010) -- 無首次資料載入(login 頁無需 prefetch) +- 401/403 server error → inline error banner(LoginForm 內,直接顯示後端回傳的 detail) +- 422 validation error(Pydantic)→ 前端已驗證故不應出現 → Error Boundary fallback +- 5xx → Error Boundary(LoginPage 層) + +Loading 策略(對應 TanStack Query 狀態欄位): +- mutation.isPending → button disabled + spinner + 全頁 pointer-events: none overlay +- GET /me isLoading → 不顯示明顯 loading(redirect check 快速完成) +- isError && !data (GET /me 5xx) → Error Boundary ``` **路由分析**: -| Path | 元件 | Route Guard | 重導向規則 | -|------|------|-------------|----------| -| `/login` | `LoginPage` | ❌ 公開 | 若已登入(authStore.token 存在)→ `/dashboard` | -| `/` | — | — | 重導向 `/login`(或 `/dashboard` 若已登入) | - ---- +| Path | 元件 | 是否需要 Route Guard | 重導向規則 | Guard 失敗行為 | +|------|------|-------------------|-----------|--------------| +| `/login` | `LoginPage` | ❌ 公開(但已登入則 redirect) | authStore.token 存在 → `/dashboard` | — | +| `/` | — | — | redirect to `/login`(或 `/dashboard` 若已登入) | — | **i18n Key 清單**(namespace: `account`): -| Key | zh-TW 預設值 | en 預設值 | 出現位置 | -|-----|------------|---------|---------| -| `login.page_title` | `Label Suite — 登入` | `Label Suite — Sign In` | `` | -| `login.subtitle` | `登入你的帳號` | `Sign in to your account` | `LoginCard` header | -| `login.google_btn` | `使用 Google 帳號繼續` | `Continue with Google` | `GoogleLoginButton` text | -| `login.google_btn_aria` | `使用 Google 帳號繼續登入` | `Continue with Google account` | `GoogleLoginButton` aria-label | -| `login.divider` | `或` | `or` | Divider | +| Key | zh-TW 預設值 | en 值 | 出現位置 | +|-----|------------|------|---------| +| `login.page_title` | `Label Suite — 登入` | `Label Suite — Sign In` | `<title>` / LoginPage | +| `login.subtitle` | `登入你的帳號` | `Sign in to your account` | LoginCard header | +| `login.google_btn` | `使用 Google 帳號繼續` | `Continue with Google` | GoogleLoginButton text | +| `login.google_btn_aria` | `使用 Google 帳號繼續登入` | `Continue with Google account` | GoogleLoginButton aria-label | +| `login.divider` | `或` | `or` | DividerWithText | | `login.email_label` | `電子郵件` | `Email` | EmailField label | | `login.email_placeholder` | `name@example.com` | `name@example.com` | EmailField placeholder | | `login.password_label` | `密碼` | `Password` | PasswordField label | @@ -375,10 +422,8 @@ Loading 策略: | `login.submit_btn` | `登入` | `Sign In` | LoginButton text | | `login.register_prompt` | `還沒有帳號?` | `Don't have an account?` | RegisterPrompt text | | `login.register_link` | `前往註冊` | `Register` | RegisterPrompt link | -| `login.email_required` | `請輸入電子郵件` | `Email is required` | EmailField error | -| `login.password_required` | `請輸入密碼` | `Password is required` | PasswordField error | -| `login.invalid_credentials` | `帳號或密碼錯誤,請再試一次` | `Invalid email or password` | Error banner | -| `login.account_disabled` | `此帳號已被停用` | `This account has been disabled` | Error banner | +| `login.email_required` | `請輸入電子郵件` | `Email is required` | EmailField error span | +| `login.password_required` | `請輸入密碼` | `Password is required` | PasswordField error span | | `login.eye_show` | `顯示密碼` | `Show password` | PasswordField eye toggle aria-label | | `login.eye_hide` | `隱藏密碼` | `Hide password` | PasswordField eye toggle aria-label | | `login.loading` | `載入中` | `Loading` | LoginButton spinner aria-label | @@ -386,17 +431,20 @@ Loading 策略: | `login.nav_aria` | `Label Suite 首頁` | `Label Suite home` | AccountNavbar brand aria-label | | `login.lang_toggle_aria` | `切換語言` | `Switch language` | LanguageToggle aria-label | -> i18n 檔案路徑:`frontend/locales/zh-TW/account.json` 與 `frontend/locales/en/account.json` - ---- - -### 4. 系統流程圖(更新) +> 前端 i18n 檔案路徑:`frontend/src/locales/zh-TW/account.json` 與 `frontend/src/locales/en/account.json` +> +> **i18n 邊界(ADR-026)**:此表僅記錄前端 UI 字串。後端 API response 的 `detail` 訊息由後端依 `Accept-Language` header 回傳,**不得**放入前端 locale 檔;前端元件直接顯示 `error.response?.data?.detail`,不做額外 key 對映。 -> 見「系統流程與資料流」章節(已包含完整 mermaid 圖)。 +**後端 i18n Key 清單**: -**Celery 分析**:本功能無非同步任務需求。 +| Key | zh-TW 預設值 | en 值 | 出現端點 | +|-----|------------|------|---------| +| `auth.invalid_credentials` | `帳號或密碼錯誤,請再試一次` | `Invalid email or password` | `POST /api/v1/auth/login` | +| `auth.account_disabled` | `此帳號已被停用,請聯繫管理員` | `This account has been disabled` | `POST /api/v1/auth/login` | +| `auth.token_expired` | `登入已過期,請重新登入` | `Session expired, please sign in again` | `GET /api/v1/auth/me` | +| `auth.token_invalid` | `驗證失敗,請重新登入` | `Authentication failed, please sign in` | `GET /api/v1/auth/me` | -- 密碼驗證(bcrypt)同步執行,P95 約 100ms,遠低於觸發 Celery 的門檻(> 1s)。 +> 後端 i18n 檔案路徑:`backend/app/i18n/zh_TW/auth.py` 與 `backend/app/i18n/en/auth.py` --- @@ -404,23 +452,27 @@ Loading 策略: | 情境 | 測試層 | 工具 | 路徑 | |------|-------|------|------| -| `hash_password` / `verify_password` 正確性 | 單元測試 | pytest | `tests/unit/test_auth_core.py` | -| `create_access_token` / `decode_access_token` | 單元測試 | pytest | `tests/unit/test_auth_core.py` | -| `POST /auth/login` 成功(回傳 token) | 整合測試 | pytest + httpx | `tests/integration/test_auth_routes.py` | -| `POST /auth/login` 錯誤密碼 → 401 | 整合測試 | pytest + httpx | `tests/integration/test_auth_routes.py` | -| `POST /auth/login` 帳號停用 → 403 | 整合測試 | pytest + httpx | `tests/integration/test_auth_routes.py` | -| `GET /auth/me` 有效 token → UserResponse | 整合測試 | pytest + httpx | `tests/integration/test_auth_routes.py` | -| `GET /auth/me` 無效 token → 401 | 整合測試 | pytest + httpx | `tests/integration/test_auth_routes.py` | -| LoginForm:email 空白送出 → 錯誤 | 元件測試 | Vitest + Testing Library | `src/features/account/__tests__/LoginForm.test.tsx` | -| LoginForm:password 空白送出 → 錯誤 | 元件測試 | Vitest + Testing Library | `src/features/account/__tests__/LoginForm.test.tsx` | -| LoginForm:重新輸入後錯誤清除 | 元件測試 | Vitest + Testing Library | `src/features/account/__tests__/LoginForm.test.tsx` | -| PasswordField:eye toggle 切換 type + aria-label | 元件測試 | Vitest + Testing Library | `src/features/account/__tests__/LoginForm.test.tsx` | -| LoginForm:API 401 → 顯示 error banner | 元件測試 | Vitest + Testing Library + msw | `src/features/account/__tests__/LoginForm.test.tsx` | -| LoginForm:送出後 button disabled + spinner | 元件測試 | Vitest + Testing Library + msw | `src/features/account/__tests__/LoginForm.test.tsx` | -| useLanguage:讀/寫 localStorage | 單元測試 | Vitest | `src/shared/__tests__/useLanguage.test.ts` | -| LoginPage:完整登入流程 → 導向 `/dashboard` | E2E | Playwright | `e2e/account/login.spec.ts` | -| LoginPage:i18n 切換後語言持久化 | E2E | Playwright | `e2e/account/login.spec.ts` | -| LoginPage:RWD 375px / 768px / 1440px | E2E | Playwright | `e2e/account/login.spec.ts` | +| `hash_password` / `verify_password` 正確性 | 單元測試 | pytest | `tests/auth/test_auth_core.py` | +| `create_access_token` / `decode_access_token` | 單元測試 | pytest | `tests/auth/test_auth_core.py` | +| `POST /auth/login` 成功(200 + TokenResponse) | 整合測試 | pytest + httpx | `tests/auth/test_auth_routes.py` | +| `POST /auth/login` 錯誤密碼 → 401 | 整合測試 | pytest + httpx | `tests/auth/test_auth_routes.py` | +| `POST /auth/login` email 不存在 → 401(防 enumeration) | 整合測試 | pytest + httpx | `tests/auth/test_auth_routes.py` | +| `POST /auth/login` 帳號停用 → 403 | 整合測試 | pytest + httpx | `tests/auth/test_auth_routes.py` | +| `GET /auth/me` 有效 token → UserResponse(無 hashed_password) | 整合測試 | pytest + httpx | `tests/auth/test_auth_routes.py` | +| `GET /auth/me` 無效 token → 401 | 整合測試 | pytest + httpx | `tests/auth/test_auth_routes.py` | +| `GET /auth/me` 過期 token → 401 | 整合測試 | pytest + httpx | `tests/auth/test_auth_routes.py` | +| `GET /auth/me` 回應不含 `hashed_password`(security gate) | 安全測試 | pytest (`@pytest.mark.security`) | `tests/auth/test_auth_routes.py` | +| `LoginForm` email 空白送出 → 錯誤顯示 | 元件測試 | Vitest + Testing Library | `src/features/account/__tests__/LoginForm.test.tsx` | +| `LoginForm` password 空白送出 → 錯誤顯示 | 元件測試 | Vitest + Testing Library | `src/features/account/__tests__/LoginForm.test.tsx` | +| `LoginForm` 重新輸入後錯誤即時清除 | 元件測試 | Vitest + Testing Library | `src/features/account/__tests__/LoginForm.test.tsx` | +| `PasswordField` eye toggle 切換 type + aria-label | 元件測試 | Vitest + Testing Library | `src/features/account/__tests__/LoginForm.test.tsx` | +| `LoginForm` API 401 → 顯示 error banner(含 detail) | 元件測試 | Vitest + Testing Library + MSW | `src/features/account/__tests__/LoginForm.test.tsx` | +| `LoginForm` 送出後 button disabled + spinner + overlay | 元件測試 | Vitest + Testing Library + MSW | `src/features/account/__tests__/LoginForm.test.tsx` | +| `useLanguage` 讀/寫 localStorage;語言切換持久化 | 單元測試 | Vitest | `src/shared/__tests__/useLanguage.test.ts` | +| `LoginPage` 完整登入流程 → 導向 `/dashboard` | E2E | Playwright | `e2e/account/login.spec.ts` | +| `LoginPage` i18n 切換後語言持久化(跨頁) | E2E | Playwright | `e2e/account/login.spec.ts` | +| `LoginPage` RWD 375px / 768px / 1440px | E2E | Playwright | `e2e/account/login.spec.ts` | +| 登入頁原型 UI presence + SC-001~SC-006 | 原型測試 | Playwright | `design/prototype/tests/account/login.spec.ts` | --- @@ -431,22 +483,50 @@ Loading 策略: **任務產生策略**: - 以 `.specify/templates/tasks-template.md` 為基礎 -- **Phase 1 — Setup**:greenfield 專案,必須先建立前後端骨架(Vite + React + Tailwind + pytest 環境),以及設計系統 token 設定 -- **Phase 2 — Foundational**:`authStore`(Zustand)、`useLanguage` hook(localStorage)、`LanguageToggle`(shared)、`User` model + migration、`core/auth.py`(hash/JWT) -- **Phase 3 — US1**(登入頁完整呈現):AccountNavbar、LoginCard、GoogleLoginButton、RegisterPrompt → 元件測試 [P] + 實作 + story -- **Phase 4 — US2**(表單互動與錯誤):LoginForm、PasswordField → 元件測試 [P] + 實作 + story -- **Phase 5 — US3**(登入送出與導頁):`auth.ts` service、`POST /auth/login` 後端路由 + service + schema → 整合測試 [P] + 實作;LoginPage mutation 串接 -- **Phase 6 — US4**(i18n + a11y):`locales/zh-TW/account.json`、`locales/en/account.json`、i18n 整合至 LoginForm + PasswordField -- **Phase 7 — US5**(RWD):Tailwind responsive 調整 + Playwright RWD E2E -- **Final Phase — Polish**:`GET /auth/me` 實作、PrivateRoute guard、Security checklist、AC checklist +- **前置確認**:列出需確認 Foundation-Core 骨架已完成的 verification task(不重複建立已有任務) +- **Phase 1 — Auth Backend Core(對應 US3)**: + - `app/core/security.py`(bcrypt hash/verify + JWT create/decode)→ 單元測試 [P] + 實作 + - `app/modules/auth/models.py`(User model + UserRole enum)→ 模型任務 [P] + - Alembic migration:upgrade() / downgrade() / roundtrip 三個嚴格順序任務 + - `app/modules/auth/repository.py`(get_user_by_email / get_user_by_id)→ 整合測試 [P] + 實作 + - `app/modules/auth/service.py`(authenticate_user)→ 整合測試 [P] + 實作 + - Backend i18n:`backend/app/i18n/zh_TW/auth.py` + `backend/app/i18n/en/auth.py`(各一任務) +- **Phase 2 — Auth API(對應 US3)**: + - `app/modules/auth/schemas.py`(LoginRequest / TokenResponse / UserBase / UserResponse)→ 測試 [P] + 實作 + - `app/core/deps.py`(get_current_user dependency)→ 整合測試 [P] + 實作 + - `app/modules/auth/router.py`(POST /login + GET /me)→ 整合測試 [P] + 實作 + - `contracts/auth-login.md` 契約文件任務 + - Bruno `.bru` skeleton 任務(post-auth-login + get-auth-me) +- **Phase 3 — Shared 前端基礎(對應 US4)**: + - `features/account/types/auth.ts`(型別定義) + - `shared/stores/authStore.ts` → 單元測試 [P] + 實作 + - `shared/hooks/useLanguage.ts` → 單元測試 [P] + 實作 + - `shared/components/LanguageToggle.tsx` → 元件測試 [P] + 實作 + story [P] + - `features/account/services/auth.ts` → 測試 [P] + 實作 + - i18n:`frontend/src/locales/zh-TW/account.json` + `frontend/src/locales/en/account.json`(各一獨立任務) +- **Phase 4 — Login UI 元件(對應 US1/US2)**: + - `AccountNavbar.tsx` → 元件測試 [P] + 實作 + story [P] + - `LoginCard.tsx` → 元件測試 [P] + 實作 + story [P] + - `GoogleLoginButton.tsx` → 元件測試 [P] + 實作 + story [P] + - `PasswordField.tsx` → 元件測試 [P] + 實作 + story [P] + - `LoginForm.tsx`(欄位驗證 + API error banner)→ 元件測試 [P] + 實作 + story [P] +- **Phase 5 — LoginPage 組裝(對應 US3/US4/US5)**: + - Route 註冊:`/login`、`/`(redirect) + - `LoginPage.tsx`(mutation 串接 + auth check redirect + RWD) + - `LoginPage.test.tsx`(整合) +- **Phase 6 — E2E + Prototype Tests(對應 SC-001~006)**: + - `design/prototype/tests/account/login.spec.ts` [P] + - `e2e/account/login.spec.ts`(完整流程 + i18n 持久化 + RWD) **排序策略**: -- TDD 順序:`tests/unit/test_auth_core.py` → `core/auth.py`;`test_auth_routes.py` → `routes/auth.py`;`LoginForm.test.tsx` → `LoginForm.tsx` -- 相依順序:`User` model + migration → `core/auth.py` → `auth_service.py` → `routes/auth.py` → 前端 `auth.ts` → `LoginForm` mutation -- 前後端獨立任務可標記 [P](model 建立與 LoginCard 元件測試可平行) +- TDD 順序:測試任務在實作任務前,各自獨立 commit,先確認失敗再實作 +- 相依順序:User model + migration → core/security → auth repository → auth service → auth schemas → auth routes → 前端 auth.ts types → authStore → useLanguage → LanguageToggle → AccountNavbar / LoginCard / GoogleLoginButton / PasswordField / LoginForm → LoginPage +- 後端 Phase 1/2 與前端 Phase 3/4 大量平行([P]) +- Migration 三任務必須嚴格順序(upgrade → downgrade → roundtrip) +- Storybook story 任務永遠為獨立 [P] 任務(testing-constitution II) -**預估產出**:`tasks.md` 中約 45–55 個有序任務 +**預估產出**:`tasks.md` 中約 55–65 個有序任務 **重要**:此階段由 `/speckit.tasks` 執行,不由 `/speckit.plan` 執行 @@ -456,8 +536,9 @@ Loading 策略: | 違反項目 | 需要原因 | 拒絕更簡單替代方案的理由 | |---------|---------|----------------------| -| Token 儲存於 localStorage(XSS 風險) | 本 spec 為 prototype → 真實系統首版;HttpOnly cookie 需要後端 CORS 設定同步更新,defer 至安全加固 spec | 短期內 XSS 風險可接受;HttpOnly 遷移為已知技術債,待後續 security spec 解決 | +| Token 儲存於 localStorage(XSS 風險) | MVP 首版;HttpOnly cookie 需要後端 CORS 同步設定,defer 至安全加固 spec | 短期 XSS 風險可接受;HttpOnly 遷移為已知技術債,待 security spec 解決 | | Plan 擴展 spec 001 未定義的後端範圍 | 使用者明確指示依真實系統撰寫;spec 001 的原型限制僅適用於 prototype 確認階段 | 若僅實作 prototype 行為,系統永遠無法進入 production | +| `auth` module 擁有 User model(可能被 admin-006 共用) | 目前僅 auth 需要 User;YAGNI 不預建 shared user module | admin-006 實作時如有需要,可依 plan-template 拆分慣例遷移 User model | --- @@ -477,7 +558,7 @@ Loading 策略: - [x] 初始憲章檢查:PASS - [x] 設計後憲章檢查:PASS - [x] 所有 NEEDS CLARIFICATION 已解決 -- [x] 複雜度偏差已記錄(localStorage token 儲存、spec 擴展) +- [x] 複雜度偏差已記錄(localStorage token、spec 擴展、User model 歸屬) --- @@ -485,4 +566,5 @@ Loading 策略: | 版本 | 日期 | 變更摘要 | |------|------|---------| +| 2.0.0 | 2026-06-09 | 完整對齊 plan-template v1.13.6:補齊 功能目標、技術方向、DB index 分析、狀態轉換、Pydantic 2b schema 表、切版分析(Stories/ARIA/響應式欄)、畫面狀態轉換、畫面×API 對應、前端技術決策、後端/前端 i18n key 清單;系統流程圖改為 module-first 架構(app/modules/auth/)含 Repository 層;Exception 設計表使用 i18n key;安全測試情境新增;憲章更新至 v1.31.0(補齊 IX、XI 檢查項;領域憲章載入節) | | 1.0.0 | 2026-05-28 | 初版 plan:涵蓋真實前後端實作(JWT auth、LoginPage API 串接),擴展 spec 001 的 prototype-only 範圍 | diff --git a/specs/foundation/000-foundation/plan.md b/specs/foundation/000-foundation/plan.md index e1b83e07..08dea049 100644 --- a/specs/foundation/000-foundation/plan.md +++ b/specs/foundation/000-foundation/plan.md @@ -1,7 +1,7 @@ --- 功能分支: feat/foundation/000-foundation 建立日期: 2026-06-05 -版本: 1.0.3 +版本: 2.0.0 狀態: plan-ready --- @@ -33,22 +33,22 @@ ## 憲章檢查 - [x] 功能目標:本計畫的功能目標與 spec.md 一致(health check 驗證端點為使用者明確要求的工程驗證工具,不修改 spec domain 範圍) -- [x] I. Spec-First:Foundation spec v1.12.0 已完成並審查 +- [x] I. Spec-First:Foundation spec v1.12.2 已完成並審查 - [x] II. Generalization-First:Foundation 骨架不含任何 task-specific 分支;所有 domain variation 透過 registry/config 擴展;`AppBaseModel` 為可繼承 base 而非特定 domain model - [x] III. Data Fairness:本計畫不涉及 test set;health check 不回傳任何敏感資料 - [x] IV. Test-First:測試計畫已列出;所有 backend schema/config/health integration tests 必須先寫並確認失敗,再實作 -- [x] V. Code Quality & Simplicity:骨架僅建立必要基礎設施;無 task-specific 假設;型別提示完整;無 debug 輸出;命名自說明 +- [x] V. Code Quality & Simplicity:骨架僅建立必要基礎設施;無 task-specific 假設;型別提示完整;無 debug 輸出;命名自說明;入口點(health router → `GET /api/v1/health`、api-client → feature service)兩層內可定位 - [x] VI. English-First:所有程式碼、注釋、commit message 使用英文 -- [x] VII. Design Consistency:HealthCheckPage 為臨時驗證工具頁,不屬於正式 UI;design system token 設定留至 foundation 完成後 -- [x] VIII. Performance Baseline:health check 無列表端點;P95 < 500ms 目標已確認 -- [x] IX. No Silent Failure:Config 缺失環境變數時 fail fast(startup validation);DB 連線失敗有結構化日誌 +- [x] VII. Design Consistency:HealthCheckPage 為 Foundation-only 內部工程驗證工具,不屬於正式 UI;不使用 MASTER.md design tokens;無 Storybook story 要求(臨時驗證元件豁免);A11y 無要求(非使用者介面);設計系統 token 設定留至 Foundation 骨架完成後 +- [x] VIII. Performance Baseline:health check 無列表端點;P95 < 500ms 目標已確認;HealthCheckPage 非正式頁面,FCP/bundle/code splitting 要求豁免 +- [x] IX. No Silent Failure:Config 缺失環境變數時 fail fast(startup validation);DB 連線失敗有結構化日誌;health check error path 有 inline error text - [x] XI. Security & Privacy Baseline:health check 不需認證;不洩漏敏感資料;CORS origins 由環境變數注入;bcrypt cost ≥ 12 ### 領域憲章載入(依觸及範圍勾選) -- [x] 後端(touches `backend/`):已讀取 `.specify/memory/backend-constitution.md`;本 plan 符合所有適用規則 -- [x] 前端(touches `frontend/`):已讀取 `.specify/memory/frontend-constitution.md`;本 plan 符合所有適用規則 -- [x] 測試(所有 task):已讀取 `.specify/memory/testing-constitution.md`;本 plan 符合所有適用規則 +- [x] 後端(touches `backend/`):已讀取 `.specify/memory/backend-constitution.md`;本 plan 符合所有適用規則(module-first 架構、async SQLAlchemy、pytest TDD、uv 命令、no hardcoded secrets) +- [x] 前端(touches `frontend/`):已讀取 `.specify/memory/frontend-constitution.md`;本 plan 符合所有適用規則(TypeScript strict、vertical feature slice、TanStack Query、Zustand、pnpm 命令) +- [x] 測試(所有 task):已讀取 `.specify/memory/testing-constitution.md`;本 plan 符合所有適用規則(Red-Green-Refactor、pytest + httpx、Vitest + MSW、每個 task 觸及一個檔案) ## 專案結構 @@ -67,6 +67,8 @@ specs/foundation/000-foundation/ ### 原始碼 > Foundation-Core 建立的是共用骨架;後續 feature module 的 `app/modules/[module]/` 與 `features/[module]/` 由各 feature PR 建立。`app/modules/health/` 為暫時驗證模組,可在系統穩定後整合至 ops endpoint。 +> +> **拆分慣例(單檔超過 300 行時):** 所有模組檔案均適用——超過 300 行時改為同名子目錄並按 feature 分檔,`__init__.py` 負責彙總對外介面,呼叫端 import 路徑不變。FR-131 路由異動偵測已擴展至涵蓋 `app/modules/*/router/` 目錄下所有子檔案。 ```text backend/ @@ -90,7 +92,7 @@ backend/ │ │ ├── correlation.py # X-Correlation-ID 生成與注入 │ │ └── logging.py # request/response 結構化日誌 │ ├── modules/ -│ │ └── health/ +│ │ └── health/ # 暫時驗證模組;最小化,僅含 router.py │ │ └── router.py # GET /api/v1/health(無認證) │ ├── schemas/ │ │ ├── base.py # AppBaseModel(model_config、datetime serialization) @@ -112,6 +114,7 @@ backend/ └── core/ ├── test_config.py # startup validation(缺失/非法 env var fail fast) ├── test_schemas.py # AppBaseModel / ErrorResponse / PaginatedResponse + ├── test_security.py # bcrypt hash / verify └── test_health.py # GET /api/v1/health integration test frontend/ @@ -169,23 +172,29 @@ frontend/ ```mermaid sequenceDiagram participant Frontend as HealthCheckPage - participant API as GET /api/v1/health participant Middleware as Correlation Middleware + participant Route as Route<br/>app/api/v1/router.py + participant Controller as Controller boundary<br/>app/modules/health/router.py Frontend->>Middleware: GET /api/v1/health - Middleware->>API: 注入 X-Correlation-ID (UUID v4) - API-->>Frontend: 200 { "status": "ok", "version": "..." }<br/>Header: X-Correlation-ID: <uuid> + Middleware->>Route: 注入 X-Correlation-ID (UUID v4) + Route->>Controller: dispatch to health module + Controller-->>Route: 200 { "status": "ok", "version": "..." } + Route-->>Frontend: 200 HealthResponse<br/>Header: X-Correlation-ID: <uuid> alt 後端未啟動 / CORS 設定錯誤 - Frontend-->>Frontend: network error → 顯示錯誤訊息 + Frontend-->>Frontend: network error → 顯示 error text end ``` +> Health check 為公開端點,無 Auth Middleware 分支。無 Service / Repository / DB 層(無 domain 資料存取)。 + | 層 | 元件 | 職責 | |----|------|------| -| Frontend | `HealthCheckPage` | 呼叫 health API、顯示 status / 錯誤訊息 | -| Middleware | `correlation.py` | 生成/傳遞 X-Correlation-ID | -| API | `modules/health/router.py` | 回傳 `{ status, version }`,不需 DB | +| Frontend | `features/health/pages/HealthCheckPage` | 呼叫 health API、顯示 status / 錯誤訊息 | +| Middleware | `middleware/correlation.py` | 生成/傳遞 X-Correlation-ID | +| Route | `api/v1/router.py` | 路由彙整 | +| Controller boundary | `modules/health/router.py` | 回傳 `{ status, version }`,不需 DB | ### Startup Validation 流程 @@ -203,7 +212,7 @@ sequenceDiagram end ``` -> **無 Celery 任務**:Foundation-Core 不包含 background job;Celery 由 Foundation-Observability 引入。 +> **本功能無非同步任務需求**:Foundation-Core 不包含 background job;Celery 由 Foundation-Observability 引入。 --- @@ -235,6 +244,8 @@ sequenceDiagram ## Phase 1:設計與契約 +> 前置條件:research.md 已完成(Phase 0 無 NEEDS CLARIFICATION,跳過) + ### 1. 共用 Schema 定義(取代 entity data model) Foundation-Core 無 domain entity,僅建立共用 schema 基礎: @@ -247,11 +258,11 @@ Foundation-Core 無 domain entity,僅建立共用 schema 基礎: | `PaginatedResponse[T]` | `AppBaseModel` | 分頁列表:`items`、`total`、`limit`、`offset`、`next_offset`、`has_more`、`total_pages` | | `HealthResponse` | `AppBaseModel` | health check 回應:`status: str`、`version: str` | -> `PaginatedResponse[T]` 的 `next_offset`、`has_more`、`total_pages` 由 `total`、`limit`、`offset` 計算(不需額外 DB 查詢)。 +> `PaginatedResponse[T]` 的 `next_offset`、`has_more`、`total_pages` 由 `total`、`limit`、`offset` 計算(不需額外 DB 查詢)。TypeVar 泛型,不用 `Any`。 -**DB Index 分析**:Foundation-Core 無 domain table;alembic/versions/ 為空。 +**DB Index 分析**:Foundation-Core 無 domain table;`alembic/versions/` 為空;本階段無 index 需求。 -**事務邊界**:health check 為 GET,無 DB write;本階段無複合事務。 +**狀態轉換**:本功能無多狀態實體。 ### 2. API 端點清單 @@ -261,27 +272,27 @@ Foundation-Core 無 domain entity,僅建立共用 schema 基礎: > health check 為 GET endpoint,冪等性天然滿足(FR-119 豁免)。 -**無複合事務**:health check 無 DB 寫入。 +**事務邊界設計**:本端點無複合事務;health check 為 GET,無 DB write。 -### 2b. Pydantic Schema 層次 +### 2b. Pydantic Schema 層次設計 -| Schema | 繼承自 | 用途 | 敏感欄位排除 | -|--------|-------|------|------------| -| `AppBaseModel` | `BaseModel` | 共用 base | — | -| `ErrorDetail` | `AppBaseModel` | validation / app rule / auth error 詳情 | — | -| `ErrorResponse` | `AppBaseModel` | 統一錯誤回應 | — | -| `PaginatedResponse[T]` | `AppBaseModel` | 分頁列表 wrapper | — | -| `HealthResponse` | `AppBaseModel` | health check 回應 | — | +| Schema | 繼承自 | 用途 | 需排除的敏感欄位 | +|--------|-------|------|----------------| +| `AppBaseModel` | `BaseModel` | 共用 base;集中 `model_config`、datetime serialization | — | +| `ErrorDetail` | `AppBaseModel` | validation / app rule / auth error 詳情;含 `loc`、`msg`、`type`、`error_code` | — | +| `ErrorResponse` | `AppBaseModel` | 統一錯誤回應;`detail: str \| list[ErrorDetail]` | — | +| `PaginatedResponse[T]` | `AppBaseModel` | 分頁列表 wrapper;TypeVar 泛型 | — | +| `HealthResponse` | `AppBaseModel` | health check 回應;`status: str`、`version: str` | — | -所有欄位均有明確型別;`PaginatedResponse[T]` 使用 TypeVar 泛型,不用 `Any`。 +> 所有欄位均有明確型別;無需 `Field(...)` constraint 或 custom validator(Foundation schema 為 base,不含業務驗證)。 ### 3. 前端切版分析 -| 區塊 | 元件名稱 | 職責 | 資料來源 | Stories 狀態 | ARIA | 響應式 | -|------|---------|------|---------|------------|------|--------| -| 頁面容器 | `HealthCheckPage` | 呼叫 health API、顯示 status | TanStack Query | — (臨時驗證頁,不寫 story) | — | 無需響應式 | +| 區塊 | 元件名稱 | 職責 | 資料來源 | Stories 狀態 | ARIA / 鍵盤需求 | 響應式行為 | +|------|---------|------|---------|------------|----------------|----------| +| 頁面容器 | `HealthCheckPage` | 呼叫 health API、顯示 status / error | TanStack Query | — (臨時驗證頁;豁免) | — (非正式 UI) | 無需響應式 | -> HealthCheckPage 為開發驗證工具,非正式 UI;無 Storybook story 要求。 +> HealthCheckPage 為開發驗證工具,非正式 UI;無 Storybook story 要求,無 ARIA / 鍵盤需求,無響應式要求。 **元件層次**: ``` @@ -291,9 +302,9 @@ HealthCheckPage **畫面狀態轉換**: -| 當前狀態 | 觸發條件 | 下一狀態 | UI 呈現 | -|--------|---------|---------|--------| -| Loading | `useQuery` isLoading | Success 或 Error | loading text | +| 當前畫面狀態 | 觸發條件 | 下一狀態 | UI 呈現 | +|------------|---------|---------|--------| +| Loading | `useQuery` isLoading | Success 或 Error | "Checking..." text | | Success | 200 回應 | 顯示 `{ status: "ok" }` | status 文字 | | Error | network error / 非 200 | 顯示錯誤訊息 | error text | @@ -306,46 +317,52 @@ HealthCheckPage **前端技術決策**: ``` -型別策略:Foundation-Core 的 `HealthResponse` 採手寫 interface(`src/shared/types/api.ts`)作為佔位;`shared/api-types/` 暫時留空,待 account/001 PR 建立 OpenAPI export 後改由 CI codegen 填入,並以 SC-018 consistency check 驗證不漂移(FR-071) +型別策略: +- [x] 手寫 interface(src/shared/types/api.ts)作為佔位 + 注意:shared/api-types/ 暫留空,待 account/001 建立 OpenAPI export 後改由 CI codegen(FR-071 / SC-018) 表單策略:無表單(health check 只有 GET) TanStack Query 策略: -- queryKey 格式:`QUERY_KEYS.health.status`(定義於 `shared/constants/query-keys.ts`,不得使用 inline string array — SC-019) +- queryKey 格式:QUERY_KEYS.health.status(定義於 shared/constants/query-keys.ts,不得使用 inline string array — SC-019) - 無 mutation,無 invalidate 需求 - 無 optimistic update API 錯誤處理策略: - network error → inline error text(不用 toast;驗證頁) -- 非 200 → inline error text - -Loading 策略: -- isLoading → "Checking..." text -- isError → error message -- data → status text +- 非 200 → inline error text +- QueryClient 的 retry callback 必須對 auth error 回傳 false(SC-020) + +Loading 策略(對應 TanStack Query 狀態欄位): +- isLoading && !data → "Checking..." text +- isFetching && data → 保留舊資料(stale-while-revalidate) +- isError && !data → error message text +- mutation.isPending → 不適用(無 mutation) ``` **路由分析**: -| Path | 元件 | Route Guard | 重導向規則 | Guard 失敗行為 | -|------|------|------------|-----------|--------------| +| Path | 元件 | 是否需要 Route Guard | 重導向規則 | Guard 失敗行為 | +|------|------|-------------------|-----------|--------------| | `/health-check` | `HealthCheckPage` | ❌ public | — | — | **i18n Key 清單**: > HealthCheckPage 為 Foundation-only 內部工程驗證工具,非正式使用者介面。其顯示字串(`status: "ok"`、error text)為技術識別符而非 UI copy,符合 frontend-constitution §IX「stable technical identifier」豁免條件。此元件在系統穩定後移除,無需建立 i18n namespace。 > -> 標記:「本功能無前端 i18n 需求 — Foundation-only 工程驗證元件,適用技術識別符豁免」 +> **標記**:「本功能無前端 i18n 需求 — Foundation-only 工程驗證元件,適用技術識別符豁免」 **後端 i18n Key 清單**: > health check 回應為固定英文字串(`status: "ok"`),不需 i18n。 > -> 標記:「本功能無新增後端訊息」 +> **標記**:「本功能無新增後端訊息」 ### 4. 系統流程圖 -詳見上方「系統流程與資料流」章節。無 Celery 任務需求:「本功能無非同步任務需求」。 +詳見上方「系統流程與資料流」章節。 + +**無非同步任務需求**:Foundation-Core 不包含 background job;Celery 由 Foundation-Observability 引入。 ### 5. 測試情境 @@ -367,13 +384,13 @@ Loading 策略: | `api-client` 從 response header 讀取 `X-Correlation-ID` | 單元測試 | Vitest | `src/shared/__tests__/api-client.test.ts` | | `QueryClient` 收到 HTTP 401 不觸發 retry(SC-020) | 單元測試 | Vitest | `src/shared/__tests__/query-client.test.ts` | -**產出**:API 清單(含 Bruno 路徑)、Pydantic schema 層次、路由分析、測試情境已概述 +**產出**:API 清單(含 Bruno 路徑)、Pydantic schema 層次、路由分析、畫面狀態轉換、畫面 × API 對應、測試情境已概述 --- ## Phase 2:任務規劃方式 -*本節描述 `/speckit.tasks` 將執行的內容* +*本節描述 `/speckit.tasks` 將執行的內容 — 不得在 `/speckit.plan` 期間執行* **User Story 結構**:Foundation-Core 以「基礎設施里程碑」而非 domain flow 組織: @@ -390,15 +407,27 @@ Loading 策略: **TDD 排序**: - 每個 task 先寫 failing test(`[P]` 標記可平行) -- backend schema tests → schema 實作 → config tests → config 實作 → health tests → health 實作 +- backend schema tests → schema 實作 → config tests → config 實作 → security tests → security 實作 → health tests → health 實作 - frontend:MSW handler + component test → HealthCheckPage 實作 +**任務產生策略**: + +- 以 `.specify/templates/tasks-template.md` 為基礎 +- 每個 API 清單項目 → 單元測試任務 [P] + 實作任務 + Bruno `.bru` 更新任務(`PR-FOUND-BRUNO`,依 FR-131) +- 每個 schema → schema 測試任務 [P] + 實作任務 +- 每個元件 → 元件測試任務 [P] + 實作任務(無 Storybook story,HealthCheckPage 豁免) +- DevOps 任務可與 backend Phase BE-1 平行執行 + **排序策略**: -- `app/schemas/` → `app/core/config.py` → `app/db/` → `app/middleware/` → `app/modules/health/` → `app/api/v1/router.py` → `app/main.py` + +- TDD 順序:測試在實作前(必須先失敗) +- `app/schemas/` → `app/core/config.py` → `app/db/` → `app/core/security.py` → `app/middleware/` → `app/modules/health/` → `app/api/v1/router.py` → `app/main.py` - frontend scaffold 可與 backend 平行執行 **預估產出**:`tasks.md` 中約 40–50 個有序任務 +**重要**:此階段由 `/speckit.tasks` 執行,不由 `/speckit.plan` 執行 + --- ## 複雜度追蹤 @@ -407,6 +436,7 @@ Loading 策略: |---------|---------|----------------------| | SQLite 本地開發 + PostgreSQL CI | ADR-024;零摩擦啟動 | 強迫本地使用 Docker PostgreSQL 增加啟動障礙;SQLite 透過 `DATABASE_URL` 切換,不影響 production 路徑 | | Observability 延後 | 使用者確認 Foundation-Core 優先 | Prometheus/Grafana/Sentry 設定工作量大;Foundation-Core 驗證前後端接通是更緊迫目標 | +| health module 僅含 router.py | 最小化原則 | health check 無 domain 資料、無業務邏輯、無 DB 存取;service / repository / models 均不必要 | --- @@ -415,8 +445,8 @@ Loading 策略: **階段狀態**: - [x] Phase 0:研究完成(無 NEEDS CLARIFICATION) -- [x] Phase 1:設計完成(schema 定義、API 清單、測試情境) -- [ ] Phase 2:任務規劃方式已描述(待 `/speckit.tasks` 執行) +- [x] Phase 1:設計完成(schema 定義、API 清單、Pydantic 層次、切版分析、測試情境) +- [x] Phase 2:任務規劃方式已描述(待 `/speckit.tasks` 執行) - [ ] Phase 3:任務已產生(`/speckit.tasks`) - [ ] Phase 4:實作完成 - [ ] Phase 5:驗證通過(`/speckit.analyze` 零發現) @@ -424,9 +454,9 @@ Loading 策略: **把關狀態**: - [x] 初始憲章檢查:PASS -- [x] 設計後憲章檢查:PASS +- [x] 設計後憲章檢查:PASS(v2.0.0 重新驗證) - [x] 所有 NEEDS CLARIFICATION 已解決 -- [x] 複雜度偏差已記錄(SQLite 分層 + Observability 延後) +- [x] 複雜度偏差已記錄(SQLite 分層 + Observability 延後 + health module 最小化) --- @@ -434,6 +464,8 @@ Loading 策略: | 版本 | 日期 | 變更摘要 | |------|------|---------| +| 2.0.0 | 2026-06-09 | 對齊 plan-template v1.13.6(major structural update):補齊 DB Index 分析、狀態轉換、事務邊界設計、Pydantic 2b 表格、前端切版分析表格(含 Stories/ARIA/響應式欄)、畫面狀態轉換表、畫面 × API 對應表、前端技術決策 checkbox 格式、後端 i18n key 清單、拆分慣例說明;更新 constitution 檢查至 v1.13.6 模板格式(含 VII Storybook / A11y / IX / XI 細化);對應 spec.md v1.12.2 確認無功能異動;系統流程圖補齊 Route → Controller boundary 層 | +| 1.0.3 | 2026-06-05 | 補充 test_security.py 至測試情境表(bcrypt hash/verify + negative test) | | 1.0.2 | 2026-06-05 | 文件目錄補齊 `checklists/`(ac-checklist.md、security-checklist.md),對齊 plan-template 非 optional 規範 | | 1.0.1 | 2026-06-05 | Bruno API 檔案路徑改為 `backend/bruno/foundation/000-foundation/get-health.bru`,對齊模組 → 功能 → API 分層追蹤 | | 1.0.0 | 2026-06-05 | 初版:Foundation-Core 計畫;範圍限定 F-01~F-10、F-13、F-16、F-18;Observability (F-17) 與 Celery (F-12) 延後至 Foundation-Observability;加入 health check 驗證端點(使用者確認);採用 module-first 後端目錄結構(對齊 Foundation Spec 基準目錄,修正 plan-template 的 layer-first 錯誤);SQLite 本地開發 / PostgreSQL CI(ADR-024) | From ebd381318cfdd939adb1312496390cef71baddae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=B3=E6=AC=A3=E6=80=A1?= <ms.mandy610425@gmail.com> Date: Wed, 10 Jun 2026 10:23:47 +0800 Subject: [PATCH 2/3] docs: correct STATUS.md changelog wording to reflect plan v2.0.0 bumps Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --- specs/STATUS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/STATUS.md b/specs/STATUS.md index 8adae260..6280861e 100644 --- a/specs/STATUS.md +++ b/specs/STATUS.md @@ -55,8 +55,8 @@ | 日期 | 更新內容 | |------|----------| -| 2026-06-09 | Update account-001 status to `plan-ready`. | -| 2026-06-09 | Update foundation-000 status to `plan-ready`. | +| 2026-06-09 | Update account-001 plan to v2.0.0: full alignment with plan-template v1.13.6; status confirmed `plan-ready`. | +| 2026-06-09 | Update foundation-000 plan to v2.0.0: aligned with plan-template v1.13.6 (major structural update). | | 2026-06-05 | foundation-000 plan v1.0.0 created (Foundation-Core): plan-ready; scope F-01~F-10, F-13, F-16, F-18; Observability/Celery deferred; health check endpoint added. | | 2026-06-04 | Update foundation-000 to spec v1.12.0: pagination switched from page/page_size to limit/offset; PaginatedResponse next_offset added. | | 2026-06-03 | Update foundation-000 to spec v1.11.5: SC-045 naming change applied. | From f2fb2829ece014bfa1a76711a7f817bc8de4e3d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=B3=E6=AC=A3=E6=80=A1?= <ms.mandy610425@gmail.com> Date: Wed, 10 Jun 2026 10:29:32 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20address=20qodo=20review=20findings?= =?UTF-8?q?=20=E2=80=94=20timezone-aware=20timestamps=20and=20JWT-only=20s?= =?UTF-8?q?ecurity=20task=20scope?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --- specs/account/001-login-email-password/plan.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specs/account/001-login-email-password/plan.md b/specs/account/001-login-email-password/plan.md index 426dc535..4cce89b9 100644 --- a/specs/account/001-login-email-password/plan.md +++ b/specs/account/001-login-email-password/plan.md @@ -274,8 +274,8 @@ sequenceDiagram | `hashed_password` | `String` NOT NULL | bcrypt hash;不得出現於任何 response schema | | `role` | `Enum('user', 'super_admin')` DEFAULT `'user'` | 系統角色 | | `is_active` | `Boolean` DEFAULT `True` | 帳號啟用狀態 | -| `created_at` | `DateTime(timezone=True)` | 建立時間(auto utcnow) | -| `updated_at` | `DateTime(timezone=True)` | 更新時間(auto utcnow onupdate) | +| `created_at` | `DateTime(timezone=True)` | 建立時間(auto `func.now()`,timezone-aware) | +| `updated_at` | `DateTime(timezone=True)` | 更新時間(auto `func.now()` onupdate,timezone-aware) | **狀態轉換**:本功能無多狀態實體。User `is_active` 僅 True/False,由 admin-006 管理,不在本 spec 範圍。 @@ -485,7 +485,7 @@ Loading 策略(對應 TanStack Query 狀態欄位): - 以 `.specify/templates/tasks-template.md` 為基礎 - **前置確認**:列出需確認 Foundation-Core 骨架已完成的 verification task(不重複建立已有任務) - **Phase 1 — Auth Backend Core(對應 US3)**: - - `app/core/security.py`(bcrypt hash/verify + JWT create/decode)→ 單元測試 [P] + 實作 + - `app/core/security.py`(新增 JWT create/decode;bcrypt hash/verify 已由 Foundation-Core 建立,不重複實作)→ 單元測試 [P] + 實作 - `app/modules/auth/models.py`(User model + UserRole enum)→ 模型任務 [P] - Alembic migration:upgrade() / downgrade() / roundtrip 三個嚴格順序任務 - `app/modules/auth/repository.py`(get_user_by_email / get_user_by_id)→ 整合測試 [P] + 實作