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)
-
登入憑證
- Email: 不區分大小寫(登入時自動轉換為小寫)
- 密碼: 區分大小寫
-
Token 管理
- Access Token:
- 有效期: 15 分鐘
- 儲存位置: Memory (Redux Store)
- 用途: 所有 API 請求的身份驗證
- Refresh Token:
- 有效期: 7 天(「記住我」時為 30 天)
- 儲存位置: HttpOnly Cookie(防止 XSS 攻擊)
- 用途: 刷新 Access Token
- Access Token:
-
帳號鎖定規則
- 連續登入失敗 5 次 → 鎖定 15 分鐘
- 鎖定期間計數器不重置(避免暴力破解)
- 15 分鐘後自動解鎖,計數器歸零
- 成功登入後計數器歸零
-
安全性規則
- 登入失敗時,錯誤訊息統一為「Email 或密碼錯誤」(不洩漏用戶是否存在)
- 所有登入請求必須透過 HTTPS 傳輸
- 實施 Rate Limiting:單一 IP 每分鐘最多 5 次登入請求
-
多租戶隔離
- 登入後 Access Token 中包含
tenantId - 用戶只能登入其所屬租戶的系統
- 跨租戶登入視為無效憑證
- 登入後 Access Token 中包含
-
審計日誌
- 所有登入嘗試(成功與失敗)都記錄至審計日誌
- 記錄內容:時間戳、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/form的Input組件 - 支援
autoComplete(用戶名欄位username,密碼欄位current-password) - 密碼欄位右側有顯示/隱藏切換按鈕(
Eye/EyeOffLucide 圖示) - 使用 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()驗證器 - 必填欄位:
username、password - 驗證失敗時欄位顯示紅色邊框和錯誤訊息
- 前端僅驗證必填,格式驗證由後端處理
按鈕狀態:
- 預設: 啟用狀態(藍色,
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/form的Input組件(用戶名、密碼)@appfuse/appfuse-web/form的Checkbox組件(記住我)@appfuse/appfuse-web/form的schema和validatorResolver(表單驗證)@appfuse/appfuse-web/components的Button組件(登入按鈕、連結按鈕)@appfuse/appfuse-web/messaging的prompt工具(Toast 通知)@appfuse/appfuse-web/hooks的useTimeoutHook(延遲導航)@appfuse/appfuse-web/utils的i18n工具(國際化)lucide-react的Eye/EyeOff圖示(密碼顯示切換)
- 自定義組件:
LoginPage- 位於src/pages/login-page.tsx(注意:採用 kebab-case 命名)
狀態管理
- Redux Slice:
src/features/iam/me-slice.ts- 儲存用戶資訊(id, email, name, role)與 Access Token
- 提供
loginThunk 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 天)
- 實作
POST /api/v1/auth/loginAPI - JWT Token 生成邏輯(Access Token + Refresh Token)
- 帳號鎖定邏輯(Redis 計數器)
- Rate Limiting 中間件
- 審計日誌記錄
- 實作
-
前端開發 (1-1.5 天)
- 創建
LoginPage與LoginForm組件 - 表單驗證邏輯(使用 React Hook Form + Zod)
- Redux Slice 整合(儲存用戶狀態與 Token)
- 路由保護邏輯(ProtectedRoute 組件)
- 錯誤處理與 Toast 通知
- 創建
-
整合與測試 (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/loginAPI 端點(使用 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