Files
hr-portal/backend/app/api/v1/lifecycle.py
Porsche Chen 360533393f 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>
2026-02-23 20:12:43 +08:00

177 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
員工生命週期管理 API
觸發員工到職、離職自動化流程
"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.models.employee import Employee
from app.services.employee_lifecycle import get_employee_lifecycle_service
router = APIRouter()
@router.post("/employees/{employee_id}/onboard")
async def onboard_employee(
employee_id: int,
create_keycloak: bool = True,
create_email: bool = True,
create_drive: bool = True,
db: Session = Depends(get_db),
):
"""
觸發員工到職流程
自動執行:
- 建立 Keycloak SSO 帳號
- 建立主要郵件帳號
- 建立雲端硬碟帳號 (Drive Service非致命)
參數:
- create_keycloak: 是否建立 Keycloak 帳號 (預設: True)
- create_email: 是否建立郵件帳號 (預設: True)
- create_drive: 是否建立雲端硬碟帳號 (預設: TrueDrive Service 未上線時自動跳過)
"""
employee = db.query(Employee).filter(Employee.id == employee_id).first()
if not employee:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"員工 ID {employee_id} 不存在"
)
if employee.status != "active":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"只能為在職員工執行到職流程 (目前狀態: {employee.status})"
)
lifecycle_service = get_employee_lifecycle_service()
results = await lifecycle_service.onboard_employee(
db=db,
employee=employee,
create_keycloak=create_keycloak,
create_email=create_email,
create_drive=create_drive,
)
return {
"message": "員工到職流程已觸發",
"employee": {
"id": employee.id,
"employee_id": employee.employee_id,
"legal_name": employee.legal_name,
},
"results": results,
}
@router.post("/employees/{employee_id}/offboard")
async def offboard_employee(
employee_id: int,
disable_keycloak: bool = True,
email_handling: str = "forward", # "forward" 或 "disable"
disable_drive: bool = True,
db: Session = Depends(get_db),
):
"""
觸發員工離職流程
自動執行:
- 停用 Keycloak SSO 帳號
- 處理郵件帳號 (轉發或停用)
- 停用雲端硬碟帳號 (Drive Service非致命)
參數:
- disable_keycloak: 是否停用 Keycloak 帳號 (預設: True)
- email_handling: 郵件處理方式 "forward""disable" (預設: forward)
- disable_drive: 是否停用雲端硬碟帳號 (預設: TrueDrive Service 未上線時自動跳過)
"""
employee = db.query(Employee).filter(Employee.id == employee_id).first()
if not employee:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"員工 ID {employee_id} 不存在"
)
if email_handling not in ["forward", "disable"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="email_handling 必須是 'forward''disable'"
)
lifecycle_service = get_employee_lifecycle_service()
results = await lifecycle_service.offboard_employee(
db=db,
employee=employee,
disable_keycloak=disable_keycloak,
handle_email=email_handling,
disable_drive=disable_drive,
)
# 將員工狀態設為離職
employee.status = "terminated"
db.commit()
return {
"message": "員工離職流程已觸發",
"employee": {
"id": employee.id,
"employee_id": employee.employee_id,
"legal_name": employee.legal_name,
},
"results": results,
}
@router.get("/employees/{employee_id}/lifecycle-status")
async def get_lifecycle_status(
employee_id: int,
db: Session = Depends(get_db),
):
"""
查詢員工的生命週期狀態
回傳:
- Keycloak 帳號狀態
- 郵件帳號狀態
- 雲端硬碟帳號狀態 (Drive Service)
"""
employee = db.query(Employee).filter(Employee.id == employee_id).first()
if not employee:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"員工 ID {employee_id} 不存在"
)
# TODO: 實際查詢各系統的帳號狀態
return {
"employee": {
"id": employee.id,
"employee_id": employee.employee_id,
"legal_name": employee.legal_name,
"status": employee.status,
},
"systems": {
"keycloak": {
"has_account": False,
"is_enabled": False,
"message": "尚未整合 Keycloak API",
},
"email": {
"has_account": False,
"email_address": f"{employee.username_base}@porscheworld.tw",
"message": "尚未整合 MailPlus API",
},
"drive": {
"has_account": False,
"drive_url": "https://drive.ease.taipei",
"message": "Drive Service 尚未上線",
},
},
}