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

View File

@@ -0,0 +1,127 @@
"""
系統權限管理 API 測試 (6.2)
測試 /api/v1/permissions/ CRUD 操作
"""
import pytest
class TestPermissionsAPI:
"""系統權限 API 測試"""
def test_get_permissions_empty(self, client):
"""空資料庫回傳空列表"""
response = client.get("/api/v1/permissions/")
assert response.status_code == 200
data = response.json()
assert data["total"] == 0
def test_get_available_systems(self, client):
"""取得可授權系統清單"""
response = client.get("/api/v1/permissions/systems")
assert response.status_code == 200
data = response.json()
assert "systems" in data
assert "gitea" in data["systems"]
assert "keycloak" in data["systems"]
assert "access_levels" in data
assert "admin" in data["access_levels"]
def test_create_permission_success(self, client, sample_employee):
"""成功建立權限"""
payload = {
"employee_id": sample_employee.id,
"system_name": "gitea",
"access_level": "user",
}
response = client.post("/api/v1/permissions/", json=payload)
assert response.status_code == 201
data = response.json()
assert data["system_name"] == "gitea"
assert data["access_level"] == "user"
assert data["employee_id"] == sample_employee.id
def test_create_permission_duplicate(self, client, sample_employee):
"""重複建立同系統權限應回傳 400"""
payload = {
"employee_id": sample_employee.id,
"system_name": "gitea",
"access_level": "user",
}
# 第一次建立
client.post("/api/v1/permissions/", json=payload)
# 第二次建立同系統
response = client.post("/api/v1/permissions/", json=payload)
assert response.status_code == 400
def test_create_permission_invalid_system(self, client, sample_employee):
"""無效系統名稱應回傳 422"""
payload = {
"employee_id": sample_employee.id,
"system_name": "invalid_system",
"access_level": "user",
}
response = client.post("/api/v1/permissions/", json=payload)
assert response.status_code == 422
def test_get_employee_permissions(self, client, sample_employee):
"""取得員工所有權限"""
# 先建立兩個權限
for system in ["gitea", "portainer"]:
client.post("/api/v1/permissions/", json={
"employee_id": sample_employee.id,
"system_name": system,
"access_level": "user",
})
response = client.get(f"/api/v1/permissions/employees/{sample_employee.id}/permissions")
assert response.status_code == 200
data = response.json()
assert len(data) == 2
systems = [p["system_name"] for p in data]
assert "gitea" in systems
assert "portainer" in systems
def test_update_permission(self, client, sample_employee):
"""更新權限層級"""
# 建立權限
create_resp = client.post("/api/v1/permissions/", json={
"employee_id": sample_employee.id,
"system_name": "gitea",
"access_level": "user",
})
perm_id = create_resp.json()["id"]
# 更新為 admin
response = client.put(f"/api/v1/permissions/{perm_id}", json={"access_level": "admin"})
assert response.status_code == 200
assert response.json()["access_level"] == "admin"
def test_delete_permission(self, client, sample_employee):
"""刪除權限"""
create_resp = client.post("/api/v1/permissions/", json={
"employee_id": sample_employee.id,
"system_name": "gitea",
"access_level": "user",
})
perm_id = create_resp.json()["id"]
response = client.delete(f"/api/v1/permissions/{perm_id}")
assert response.status_code == 200
# 確認已刪除
get_resp = client.get(f"/api/v1/permissions/{perm_id}")
assert get_resp.status_code == 404
def test_batch_create_permissions(self, client, sample_employee):
"""批次建立權限"""
payload = {
"employee_id": sample_employee.id,
"permissions": [
{"system_name": "gitea", "access_level": "user"},
{"system_name": "portainer", "access_level": "readonly"},
],
}
response = client.post("/api/v1/permissions/batch", json=payload)
assert response.status_code == 201
data = response.json()
assert len(data) == 2