跳至主要内容

US-001: 用戶登入

User Story

作為 花店員工(店主、店員、設計師、送貨員) 我想要 能夠使用 Email 和密碼登入系統 以便 訪問與我的角色相對應的系統功能


驗收標準 (Acceptance Criteria)

Scenario 1: 使用有效憑證登入成功

  • Given 我在登入頁面
  • And 系統中有註冊用戶「staff@florist.com」密碼「Password123!」角色「店員」
  • When 我輸入 Email「staff@florist.com
  • And 我輸入密碼「Password123!」
  • And 我點擊「登入」按鈕
  • Then 系統應驗證我的憑證
  • And 系統應生成 Access Token(有效期 15 分鐘)和 Refresh Token(有效期 7 天)
  • And 系統應將我重定向至首頁
  • And 我應在右上角看到我的用戶名稱與角色標籤
  • And 系統應記錄登入日誌(包含 IP 地址、裝置資訊、時間戳)

Scenario 2: 使用錯誤密碼登入失敗

  • Given 我在登入頁面
  • And 系統中有註冊用戶「staff@florist.com」密碼「Password123!」
  • When 我輸入 Email「staff@florist.com
  • And 我輸入錯誤密碼「WrongPassword」
  • And 我點擊「登入」按鈕
  • Then 系統應顯示錯誤訊息「Email 或密碼錯誤」
  • And 系統應不發放 Token
  • And 我應仍然停留在登入頁面
  • And Email 欄位應保留我輸入的值
  • And 密碼欄位應清空
  • And 系統應記錄登入失敗日誌

Scenario 3: 使用不存在的 Email 登入失敗

  • Given 我在登入頁面
  • When 我輸入不存在的 Email「nonexistent@example.com
  • And 我輸入任意密碼「Password123!」
  • And 我點擊「登入」按鈕
  • Then 系統應顯示錯誤訊息「Email 或密碼錯誤」(與錯誤密碼相同,避免洩漏用戶存在資訊)
  • And 系統應不發放 Token
  • And 系統應記錄登入失敗日誌

Scenario 4: 使用「記住我」功能登入

  • Given 我在登入頁面
  • And 系統中有註冊用戶「staff@florist.com
  • When 我輸入正確的 Email 和密碼
  • And 我勾選「記住我」選項
  • And 我點擊「登入」按鈕
  • Then 系統應發放 Refresh Token,有效期延長至 30 天(而非 7 天)
  • And 系統應在瀏覽器中儲存「記住我」的偏好設定
  • And 下次我訪問登入頁面時,Email 欄位應自動填入

Scenario 5: 登入失敗 5 次後帳號鎖定

  • Given 我在登入頁面
  • And 系統中有註冊用戶「staff@florist.com
  • And 我已經連續登入失敗 4 次
  • When 我第 5 次輸入錯誤密碼
  • And 我點擊「登入」按鈕
  • Then 系統應鎖定帳號 15 分鐘
  • And 系統應顯示錯誤訊息「帳號已被鎖定,請 15 分鐘後再試」
  • And 在鎖定期間,即使輸入正確密碼也無法登入
  • And 系統應發送通知給用戶(Email)告知帳號被鎖定
  • And 系統應記錄帳號鎖定事件至審計日誌

Scenario 6: 表單驗證 - Email 格式錯誤(客戶端驗證)

  • Given 我在登入頁面
  • When 我輸入無效的 Email 格式「invalid-email」
  • And 我將焦點移到密碼欄位(失去焦點)
  • Then Email 欄位應顯示紅色邊框
  • And 欄位下方應顯示錯誤訊息「請輸入有效的 Email 地址」
  • And 「登入」按鈕應保持禁用狀態

Scenario 7: 表單驗證 - 必填欄位為空(客戶端驗證)

  • Given 我在登入頁面
  • When 我將 Email 欄位留空
  • And 我點擊「登入」按鈕
  • Then Email 欄位應顯示紅色邊框
  • And 欄位下方應顯示錯誤訊息「Email 為必填欄位」
  • And 系統不應發送 API 請求

業務規則 (Business Rules)

  1. 登入憑證

    • Email: 不區分大小寫(登入時自動轉換為小寫)
    • 密碼: 區分大小寫
  2. Token 管理

    • Access Token:
      • 有效期: 15 分鐘
      • 儲存位置: Memory (Redux Store)
      • 用途: 所有 API 請求的身份驗證
    • Refresh Token:
      • 有效期: 7 天(「記住我」時為 30 天)
      • 儲存位置: HttpOnly Cookie(防止 XSS 攻擊)
      • 用途: 刷新 Access Token
  3. 帳號鎖定規則

    • 連續登入失敗 5 次 → 鎖定 15 分鐘
    • 鎖定期間計數器不重置(避免暴力破解)
    • 15 分鐘後自動解鎖,計數器歸零
    • 成功登入後計數器歸零
  4. 安全性規則

    • 登入失敗時,錯誤訊息統一為「Email 或密碼錯誤」(不洩漏用戶是否存在)
    • 所有登入請求必須透過 HTTPS 傳輸
    • 實施 Rate Limiting:單一 IP 每分鐘最多 5 次登入請求
  5. 多租戶隔離

    • 登入後 Access Token 中包含 tenantId
    • 用戶只能登入其所屬租戶的系統
    • 跨租戶登入視為無效憑證
  6. 審計日誌

    • 所有登入嘗試(成功與失敗)都記錄至審計日誌
    • 記錄內容:時間戳、Email、IP 地址、User Agent、結果(成功/失敗原因)

UI/UX 需求 (UI/UX Requirements)

頁面佈局

┌────────────────────────────────────────┐
│ │
│ [花店 Logo 圖示] │
│ │
│ 花店管理系統 │
│ 登入以開始管理您的花店業務 │
│ │
│ ┌────────────────────────────┐ │
│ │ 歡迎回來 │ │
│ │ 請輸入您的帳號資訊 │ │
│ │ │ │
│ │ 用戶名 │ │
│ │ ┌────────────────────┐ │ │
│ │ │ 請輸入用戶名或Email │ │ │
│ │ └────────────────────┘ │ │
│ │ │ │
│ │ 密碼 │ │
│ │ ┌────────────────────┬─┐ │ │
│ │ │ 請輸入密碼 │👁│ │ │
│ │ └────────────────────┴─┘ │ │
│ │ │ │
│ │ ☐ 記住我 忘記密碼? │ │
│ │ │ │
│ │ [ 登入 ] │ │
│ │ │ │
│ │ 還沒有帳號? 立即註冊 │ │
│ └────────────────────────────┘ │
│ │
│ © 2025 花店管理系統. 版權所有. │
│ │
│ [開發環境] 測試帳號提示 │
│ 帳號: staff@florist.com │
│ 密碼: Password123! │
│ │
└────────────────────────────────────────┘

當前實作特性

品牌識別區:

  • Logo: 圓角方形背景(bg-primary/10)內含 SVG 圖示
  • 標題: 使用 text-3xl text-base-content
  • 副標題: 使用 text-base-content/60 低對比度

表單卡片:

  • 使用 DaisyUI card 組件(bg-base-100 shadow-xl
  • 內含歡迎文字和登入提示

輸入欄位:

  • 使用 @appfuse/appfuse-web/formInput 組件
  • 支援 autoComplete(用戶名欄位 username,密碼欄位 current-password
  • 密碼欄位右側有顯示/隱藏切換按鈕(Eye/EyeOff Lucide 圖示)
  • 使用 React Hook Form + 自定義 validatorResolver 驗證
  • 欄位支援 required 屬性與 aria-label

按鈕與連結:

  • 記住我: 使用 Checkbox 組件,density="compact"
  • 忘記密碼: 使用 Button 組件,variant="link"(Phase 2 功能)
  • 登入按鈕: 使用 Button 組件,color="primary",全寬 (w-full)
  • 註冊連結: 使用 Button 組件,variant="link"(Phase 2 功能)

載入狀態:

  • 按鈕顯示 loading-spinner + 「登入中...」文字
  • 提交期間按鈕 disabled

訊息通知:

  • 使用 prompt.success() / prompt.error() / prompt.info() 顯示 Toast
  • 成功登入後 500ms 延遲導航至首頁

開發環境提示:

  • 僅在 import.meta.env.DEV 為 true 時顯示
  • 使用 DaisyUI alert alert-info 樣式
  • 顯示測試帳號資訊

互動行為

輸入欄位:

  • 用戶名: 支援自動完成(瀏覽器預填),支援 email 或用戶名格式
  • 密碼: 顯示/隱藏密碼切換按鈕(Eye/EyeOff 圖示)
  • 用戶名欄位自動獲得焦點(autoFocus

表單驗證:

  • 使用自定義 schema.object() 驗證器
  • 必填欄位: usernamepassword
  • 驗證失敗時欄位顯示紅色邊框和錯誤訊息
  • 前端僅驗證必填,格式驗證由後端處理

按鈕狀態:

  • 預設: 啟用狀態(藍色,color="primary"
  • 提交中: Loading 狀態(spinner + 文字「登入中...」)
  • 表單無效時: 提交會觸發驗證錯誤顯示

錯誤提示:

  • 欄位驗證錯誤: 紅色邊框 + 欄位下方紅色文字
  • API 錯誤: prompt.error() Toast 通知

成功反饋:

  • prompt.success() Toast 通知「登入成功!」
  • 500ms 後自動重定向至首頁

響應式設計

  • 所有尺寸: 居中卡片佈局(max-w-md),bg-base-200 背景
  • 容器: min-h-screen flex items-center justify-center p-4
  • 手機版: 表單自動適應螢幕寬度

無障礙性 (Accessibility)

  • 支援鍵盤導航(Tab 鍵切換欄位,Enter 鍵提交)
  • 所有欄位有正確的 aria-label 屬性
  • 密碼切換按鈕有 aria-label(「顯示密碼」/「隱藏密碼」)
  • 表單使用 noValidate 防止瀏覽器原生驗證干擾
  • 登入按鈕有 aria-label

國際化 (i18n)

  • 所有文字使用 i18n.t() 函數
  • 支援繁體中文和英文

技術規格 (Technical Specifications)

API 端點

  • 端點: POST /api/v1/auth/login
  • 權限要求: 公開(無需認證)
  • Rate Limiting: 5 次/分鐘/IP

請求 Payload

interface LoginRequest {
email: string; // Email 地址(自動轉換為小寫)
password: string; // 密碼(原始文字)
rememberMe?: boolean; // 「記住我」選項(預設 false)
}

請求範例

POST /api/v1/auth/login HTTP/1.1
Content-Type: application/json

{
"email": "staff@florist.com",
"password": "Password123!",
"rememberMe": true
}

成功響應 (200 OK)

{
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "user-001",
"email": "staff@florist.com",
"name": "王小明",
"roles": ["ROLE_STAFF"],
"tenantId": "tenant-abc",
"tenantName": "花店 ABC"
},
"expiresIn": 900 // Access Token 有效期(秒),900 = 15 分鐘
}

錯誤響應

401 Unauthorized - 憑證錯誤

{
"error": "INVALID_CREDENTIALS",
"message": "Email 或密碼錯誤",
"timestamp": "2025-10-31T10:30:00Z"
}

423 Locked - 帳號鎖定

{
"error": "ACCOUNT_LOCKED",
"message": "帳號已被鎖定,請 15 分鐘後再試",
"lockedUntil": "2025-10-31T10:45:00Z",
"timestamp": "2025-10-31T10:30:00Z"
}

429 Too Many Requests - Rate Limit

{
"error": "RATE_LIMIT_EXCEEDED",
"message": "請求過於頻繁,請稍後再試",
"retryAfter": 60, // 秒
"timestamp": "2025-10-31T10:30:00Z"
}

數據模型

  • 主要 Entity: User
  • 關聯 Entity: Tenant, Role
  • 詳細定義: 參考用戶數據模型(data-models/user-role.md 待建立)

前端組件

  • 使用框架組件:
    • @appfuse/appfuse-web/formInput 組件(用戶名、密碼)
    • @appfuse/appfuse-web/formCheckbox 組件(記住我)
    • @appfuse/appfuse-web/formschemavalidatorResolver(表單驗證)
    • @appfuse/appfuse-web/componentsButton 組件(登入按鈕、連結按鈕)
    • @appfuse/appfuse-web/messagingprompt 工具(Toast 通知)
    • @appfuse/appfuse-web/hooksuseTimeout Hook(延遲導航)
    • @appfuse/appfuse-web/utilsi18n 工具(國際化)
    • lucide-reactEye/EyeOff 圖示(密碼顯示切換)
  • 自定義組件:
    • LoginPage - 位於 src/pages/login-page.tsx(注意:採用 kebab-case 命名)

狀態管理

  • Redux Slice: src/features/iam/me-slice.ts
    • 儲存用戶資訊(id, email, name, role)與 Access Token
    • 提供 login Thunk action(調用 AuthService)
  • AuthService: src/services/iam/auth-service.ts
    • 封裝登入 API 調用
    • 使用 ajax 客戶端處理 HTTP 請求

路由保護

  • 公開路由: /login, /forgot-password
  • 受保護路由: 所有其他路由(未登入時重定向至 /login
  • 實作位置: src/routes/ProtectedRoute.tsx

SBE 場景 (Specification by Example)

詳細的測試場景與範例數據:


估算 (Estimation)

  • Story Points: 5 點
  • 預估工時: 2-3 天
  • 複雜度: 中等

工作拆分

  1. 後端開發 (1 天)

    • 實作 POST /api/v1/auth/login API
    • JWT Token 生成邏輯(Access Token + Refresh Token)
    • 帳號鎖定邏輯(Redis 計數器)
    • Rate Limiting 中間件
    • 審計日誌記錄
  2. 前端開發 (1-1.5 天)

    • 創建 LoginPageLoginForm 組件
    • 表單驗證邏輯(使用 React Hook Form + Zod)
    • Redux Slice 整合(儲存用戶狀態與 Token)
    • 路由保護邏輯(ProtectedRoute 組件)
    • 錯誤處理與 Toast 通知
  3. 整合與測試 (0.5 天)

    • 整合測試(API + UI)
    • E2E 測試(登入成功與失敗流程)
    • Mock API 更新(支援登入端點)

依賴 (Dependencies)

前置條件

  • 後端 API: 認證端點必須先實作完成
  • Redis: 用於帳號鎖定計數器與 Token 黑名單

並行開發

  • 可與 US-002(角色權限控制)、US-003(基礎 Layout)並行開發
  • US-002 和 US-003 依賴 US-001 的認證機制

外部依賴

  • SMTP 服務(如 SendGrid): 用於發送帳號鎖定通知(Phase 2 功能)

測試策略 (Testing Strategy)

單元測試

  • 測試表單驗證邏輯(Email 格式、必填欄位)
  • 測試 Redux actions(登入、登出)
  • 測試 JWT Token 解析邏輯

整合測試

  • 測試 POST /api/v1/auth/login API 端點(使用 MSW)
  • 測試帳號鎖定邏輯(連續失敗 5 次)
  • 測試 Rate Limiting(短時間內多次請求)

E2E 測試

  • 測試完整登入流程(輸入憑證 → 提交 → 重定向至首頁)
  • 測試登入失敗流程(錯誤訊息顯示)
  • 測試「記住我」功能(下次訪問時 Email 預填)

安全性測試

  • 測試 SQL Injection(輸入惡意 Email)
  • 測試 XSS 攻擊(輸入 <script> 標籤)
  • 測試暴力破解防護(帳號鎖定)

完成定義 (Definition of Done)

  • 後端 API 實作完成並通過單元測試
  • 前端 UI 實作完成並符合設計規範
  • 整合測試通過(API + UI)
  • E2E 測試通過(至少涵蓋 Scenario 1、2、5)
  • 單元測試覆蓋率 > 80%
  • 安全性測試通過(無 SQL Injection、XSS 漏洞)
  • Code Review 通過
  • API 文檔已更新(認證 API 規格
  • Mock API 已更新(支援登入端點)
  • 審計日誌功能驗證通過
  • Rate Limiting 驗證通過
  • 無障礙性測試通過(WCAG 2.1 AA)
  • 產品經理驗收通過

備註 (Notes)

設計決策

  • 為何使用 JWT 而非 Session? JWT 無狀態,易於水平擴展,適合多租戶 SaaS 應用。
  • 為何使用 HttpOnly Cookie 儲存 Refresh Token? 防止 XSS 攻擊,提升安全性。
  • 為何統一錯誤訊息? 避免洩漏用戶是否存在,防止用戶枚舉攻擊。

未來優化

  • 支援多因素認證 (MFA)(US-012)
  • 支援社交登入(Google、Facebook)
  • 支援 SSO(Single Sign-On)
  • 實作異常登入偵測(新 IP 登入時發送通知)

最後更新: 2025-12-03