feat: HR Portal - Complete Multi-Tenant System with Redis Session Storage

Major Features:
-  Multi-tenant architecture (tenant isolation)
-  Employee CRUD with lifecycle management (onboarding/offboarding)
-  Department tree structure with email domain management
-  Company info management (single-record editing)
-  System functions CRUD (permission management)
-  Email account management (multi-account per employee)
-  Keycloak SSO integration (auth.lab.taipei)
-  Redis session storage (10.1.0.254:6379)
  - Solves Cookie 4KB limitation
  - Cross-system session sharing
  - Sliding expiration (8 hours)
  - Automatic token refresh

Technical Stack:
Backend:
- FastAPI + SQLAlchemy
- PostgreSQL 16 (10.1.0.20:5433)
- Keycloak Admin API integration
- Docker Mailserver integration (SSH)
- Alembic migrations

Frontend:
- Next.js 14 (App Router)
- NextAuth 4 with Keycloak Provider
- Redis session storage (ioredis)
- Tailwind CSS

Infrastructure:
- Redis 7 (10.1.0.254:6379) - Session + Cache
- Keycloak 26.1.0 (auth.lab.taipei)
- Docker Mailserver (10.1.0.254)

Architecture Highlights:
- Session管理由 Keycloak + Redis 統一控制
- 支援多系統 (HR/WebMail/Calendar/Drive/Office) 共享 session
- Token 自動刷新,異質服務整合
- 未來可無縫遷移到雲端

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 20:12:43 +08:00
commit 360533393f
386 changed files with 70353 additions and 0 deletions

427
KEYCLOAK_SETUP.md Normal file
View File

@@ -0,0 +1,427 @@
# Keycloak 配置指南 - HR Portal
## 📋 前置需求
- Keycloak 已運行在 `https://auth.ease.taipei`
- Realm `porscheworld` 已創建
- 管理員帳號可登入
---
## 🔧 配置步驟
### 1. 創建 HR Portal 客戶端
#### 1.1 登入 Keycloak Admin Console
1. 訪問: `https://auth.ease.taipei`
2. 點擊「Administration Console」
3. 使用管理員帳號登入
#### 1.2 選擇 Realm
1. 左上角選擇 `porscheworld` realm
2. 如果沒有,請先創建
#### 1.3 創建新 Client
1. 左側選單 → **Clients**
2. 點擊 **Create client** 按鈕
**General Settings**:
```
Client type: OpenID Connect
Client ID: hr-portal
Name: HR Portal
Description: HR Management Portal
```
點擊 **Next**
**Capability config**:
```
Client authentication: OFF (公開客戶端)
Authorization: OFF
Authentication flow:
✓ Standard flow (Authorization Code Flow)
✓ Direct access grants
```
點擊 **Next**
**Login settings**:
```
Root URL: https://hr.ease.taipei
Home URL: https://hr.ease.taipei
Valid redirect URIs:
https://hr.ease.taipei/*
http://localhost:3000/* (開發用)
Valid post logout redirect URIs:
https://hr.ease.taipei/*
http://localhost:3000/*
Web origins:
https://hr.ease.taipei
http://localhost:3000
```
點擊 **Save**
---
### 2. 配置 Client 進階設定
進入 Client `hr-portal` 的設定頁面:
#### 2.1 Settings 標籤
**Access settings**:
```
Root URL: https://hr.ease.taipei
Home URL: https://hr.ease.taipei
Valid redirect URIs: https://hr.ease.taipei/*
Valid post logout redirect URIs: https://hr.ease.taipei/*
Web origins: https://hr.ease.taipei
Admin URL: (留空)
```
**Login settings**:
```
Login theme: keycloak (或自訂主題)
Consent required: OFF
Display client on screen: ON
```
**Logout settings**:
```
Front channel logout: OFF
Backchannel logout URL: (留空)
Backchannel logout session required: ON
```
點擊 **Save**
#### 2.2 Advanced 標籤
**Advanced settings**:
```
Access Token Lifespan: 5 minutes (300 秒)
Client Session Idle: 30 minutes (1800 秒)
Client Session Max: 12 hours (43200 秒)
```
**Authentication flow overrides**:
```
Browser Flow: browser (預設)
Direct Grant Flow: direct grant (預設)
```
點擊 **Save**
---
### 3. 配置 Client Scopes
#### 3.1 查看預設 Scopes
進入 `hr-portal` Client → **Client scopes** 標籤
**Assigned default client scopes** 應包含:
- `email`
- `profile`
- `roles`
- `web-origins`
#### 3.2 建議新增的 Scopes (可選)
如需更細緻的權限控制,可創建自訂 scope:
1. 左側選單 → **Client scopes**
2. 點擊 **Create client scope**
**範例: hr-admin scope**
```
Name: hr-admin
Description: HR Portal Admin Access
Protocol: openid-connect
Display on consent screen: ON
Include in token scope: ON
```
創建後,回到 `hr-portal` Client → **Client scopes** 標籤 → 將 `hr-admin` 加入 **Assigned optional client scopes**
---
### 4. 配置用戶屬性映射 (Mappers)
進入 `hr-portal` Client → **Client scopes****hr-portal-dedicated****Mappers**
#### 4.1 確認現有 Mappers
應該已有:
- `aud claim` - Audience mapping
- `Client IP Address` - Client IP
- `Client ID` - Client identifier
- `Client Host` - Client hostname
#### 4.2 新增自訂 Mapper (用於員工資訊)
點擊 **Add mapper****By configuration****User Attribute**
**Mapper 1: Employee ID**
```
Name: employee_id
User Attribute: employee_id
Token Claim Name: employee_id
Claim JSON Type: String
Add to ID token: ON
Add to access token: ON
Add to userinfo: ON
Multivalued: OFF
Aggregate attribute values: OFF
```
**Mapper 2: Business Unit**
```
Name: business_unit
User Attribute: business_unit_id
Token Claim Name: business_unit_id
Claim JSON Type: String
Add to ID token: ON
Add to access token: ON
Add to userinfo: ON
```
點擊 **Save**
---
### 5. 測試 Client 配置
#### 5.1 使用 Keycloak 內建測試工具
1. 進入 `hr-portal` Client → **Client scopes****Evaluate**
2. 選擇測試用戶 (例如: testuser)
3. 點擊 **Generated access token**
4. 檢查 token payload 是否包含所需 claims
#### 5.2 測試 OIDC Discovery
在瀏覽器或命令列執行:
```bash
curl https://auth.ease.taipei/realms/porscheworld/.well-known/openid-configuration
```
應返回完整的 OIDC 配置,包含:
- `issuer`
- `authorization_endpoint`
- `token_endpoint`
- `userinfo_endpoint`
- `end_session_endpoint`
---
## 🧪 測試登入流程
### 測試 1: 前端登入測試
1. 訪問 `https://hr.ease.taipei` (或 `http://localhost:3000`)
2. 系統自動重定向到 Keycloak 登入頁面
3. 輸入測試用戶帳號密碼
4. 登入成功後重定向回 HR Portal
5. 檢查 localStorage 中是否有 `kc-token`
### 測試 2: Token 驗證
在瀏覽器 Console 執行:
```javascript
// 獲取 token
const keycloak = window.keycloak;
console.log('Access Token:', keycloak.token);
console.log('ID Token:', keycloak.idToken);
console.log('Refresh Token:', keycloak.refreshToken);
// 解析 token
const payload = JSON.parse(atob(keycloak.token.split('.')[1]));
console.log('Token Payload:', payload);
```
檢查 payload 應包含:
- `sub`: User ID
- `email`: 用戶 email
- `preferred_username`: 用戶名
- `given_name`, `family_name`: 姓名
- `employee_id`, `business_unit_id`: 自訂屬性 (如已配置)
---
## 🔐 安全設定建議
### 1. Token 生命週期
建議設定:
```
Access Token Lifespan: 5 minutes
SSO Session Idle: 30 minutes
SSO Session Max: 12 hours
Refresh Token Max Reuse: 0 (單次使用)
```
### 2. 密碼策略
Realm Settings → **Authentication****Password Policy**
建議啟用:
- Minimum Length: 8
- Not Username
- Uppercase Characters: 1
- Lowercase Characters: 1
- Digits: 1
- Special Characters: 1
- Not Recently Used: 3
### 3. 啟用 MFA (多因素認證)
Realm Settings → **Authentication****Flows**
編輯 **Browser** flow:
1. 找到 `Browser - Conditional OTP`
2.**Requirement** 改為 **Required**
3. 點擊 **Save**
用戶首次登入時會要求設定 OTP (Google Authenticator / FreeOTP)
---
## 👥 測試用戶管理
### 創建測試用戶
1. 左側選單 → **Users**
2. 點擊 **Add user**
**User details**:
```
Username: test.employee
Email: test.employee@porscheworld.tw
First name: Test
Last name: Employee
Email verified: ON
Enabled: ON
```
3. 點擊 **Create**
**設定密碼**:
1. 進入用戶 → **Credentials** 標籤
2. 點擊 **Set password**
3. 輸入密碼 (例如: Test1234!)
4. Temporary: OFF
5. 點擊 **Save**
**設定屬性** (如需要):
1. 進入用戶 → **Attributes** 標籤
2. 新增屬性:
- Key: `employee_id`, Value: `EMP001`
- Key: `business_unit_id`, Value: `1`
3. 點擊 **Save**
---
## 🔄 同步到後端 HR 系統
### 選項 1: 手動同步
當在 Keycloak 創建用戶後,需要在 HR Portal 後端創建對應的員工記錄:
```bash
# 使用 API 創建員工
curl -X POST https://hr-api.ease.taipei/api/v1/employees/ \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"employee_id": "EMP001",
"username": "test.employee",
"keycloak_user_id": "KEYCLOAK_USER_UUID",
"first_name": "Test",
"last_name": "Employee",
"chinese_name": "測試員工",
"email": "test.employee@porscheworld.tw",
"business_unit_id": 1,
"position": "Test Engineer",
"job_level": "Staff",
"hire_date": "2024-01-01",
"status": "active"
}'
```
### 選項 2: 自動同步 (使用 Keycloak Event Listener)
在後端實現 Keycloak Event Listener (進階功能):
1. 監聽 Keycloak 的 `REGISTER``UPDATE_PROFILE` 事件
2. 當用戶註冊或更新資料時,自動在 HR 資料庫創建/更新記錄
3. 使用 Keycloak Admin API 獲取用戶資訊
---
## 📝 配置檢查清單
部署前確認:
- [ ] Client `hr-portal` 已創建
- [ ] Client Type: OpenID Connect
- [ ] Client authentication: OFF (公開客戶端)
- [ ] Standard flow: ✓ Enabled
- [ ] Valid redirect URIs: `https://hr.ease.taipei/*`
- [ ] Web origins: `https://hr.ease.taipei`
- [ ] Access token lifespan: 5 minutes
- [ ] 測試用戶已創建並可登入
- [ ] Token 包含必要的 claims
- [ ] 前端可成功登入並獲取 token
- [ ] 前端可使用 token 調用後端 API
---
## 🐛 常見問題
### 問題 1: Invalid redirect URI
**原因**: 前端重定向 URI 不在 Valid redirect URIs 列表中
**解決**:
1. 檢查前端配置的 `VITE_KEYCLOAK_URL`
2. 確認 Keycloak Client 的 Valid redirect URIs 包含前端 URL
### 問題 2: CORS 錯誤
**原因**: Web origins 未正確配置
**解決**:
在 Keycloak Client 設定中,確保 Web origins 包含前端域名:
```
https://hr.ease.taipei
```
### 問題 3: Token 過期過快
**原因**: Access token lifespan 設定過短
**解決**:
調整 Client → Advanced → Access Token Lifespan
建議: 5-15 分鐘
### 問題 4: 用戶無法登入
**檢查**:
1. 用戶是否已啟用 (Enabled: ON)
2. Email 是否已驗證 (Email verified: ON)
3. 密碼是否正確設定
4. Client 的 Authentication flow 是否啟用
---
## 📚 參考資料
- [Keycloak Documentation](https://www.keycloak.org/documentation)
- [OIDC Specification](https://openid.net/specs/openid-connect-core-1_0.html)
- [Keycloak Admin REST API](https://www.keycloak.org/docs-api/latest/rest-api/index.html)