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,115 @@
"""cleanup and finalize
Revision ID: 0011
Revises: 0010
Create Date: 2026-02-20
架構收尾與清理:
1. 移除廢棄表business_units, employee_identities
2. 為 tenant_role_rights 新增 is_active
3. 重新命名觸發器org_* → tenant_*
"""
from alembic import op
import sqlalchemy as sa
revision = '0011'
down_revision = '0010'
branch_labels = None
depends_on = None
def upgrade() -> None:
# =========================================================
# Phase 1: 移除廢棄表(先移除外鍵約束)
# =========================================================
# 1.1 先移除依賴 business_units 的外鍵
# employee_identities.business_unit_id FK
op.drop_constraint('employee_identities_business_unit_id_fkey', 'employee_identities', type_='foreignkey')
# tenant_email_accounts.business_unit_id FK如果存在
try:
op.drop_constraint('fk_email_accounts_business_unit', 'tenant_email_accounts', type_='foreignkey')
except:
pass # 可能不存在
# 1.2 移除 employee_identities 表(已被 tenant_emp_setting 取代)
op.drop_table('employee_identities')
# 1.3 移除 business_units 表(已被 tenant_departments 取代)
op.drop_table('business_units')
# =========================================================
# Phase 2: 為 tenant_role_rights 新增 is_active
# =========================================================
op.add_column('tenant_role_rights', sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true', comment='是否啟用'))
# =========================================================
# Phase 3: 重新命名觸發器org_* → tenant_*
# =========================================================
# 3.1 tenant_departments
op.execute("""
DROP TRIGGER IF EXISTS trigger_org_departments_seq_no ON tenant_departments;
CREATE TRIGGER trigger_tenant_departments_seq_no
BEFORE INSERT ON tenant_departments
FOR EACH ROW
EXECUTE FUNCTION generate_tenant_seq_no();
""")
# 3.2 tenant_employees
op.execute("""
DROP TRIGGER IF EXISTS trigger_org_employees_seq_no ON tenant_employees;
CREATE TRIGGER trigger_tenant_employees_seq_no
BEFORE INSERT ON tenant_employees
FOR EACH ROW
EXECUTE FUNCTION generate_tenant_seq_no();
""")
# 3.3 tenant_user_roles
op.execute("""
DROP TRIGGER IF EXISTS trigger_org_user_roles_seq_no ON tenant_user_roles;
CREATE TRIGGER trigger_tenant_user_roles_seq_no
BEFORE INSERT ON tenant_user_roles
FOR EACH ROW
EXECUTE FUNCTION generate_tenant_seq_no();
""")
def downgrade() -> None:
# 恢復觸發器名稱
op.execute("DROP TRIGGER IF EXISTS trigger_tenant_user_roles_seq_no ON tenant_user_roles")
op.execute("""
CREATE TRIGGER trigger_org_user_roles_seq_no
BEFORE INSERT ON tenant_user_roles
FOR EACH ROW
EXECUTE FUNCTION generate_tenant_seq_no()
""")
op.execute("DROP TRIGGER IF EXISTS trigger_tenant_employees_seq_no ON tenant_employees")
op.execute("""
CREATE TRIGGER trigger_org_employees_seq_no
BEFORE INSERT ON tenant_employees
FOR EACH ROW
EXECUTE FUNCTION generate_tenant_seq_no()
""")
op.execute("DROP TRIGGER IF EXISTS trigger_tenant_departments_seq_no ON tenant_departments")
op.execute("""
CREATE TRIGGER trigger_org_departments_seq_no
BEFORE INSERT ON tenant_departments
FOR EACH ROW
EXECUTE FUNCTION generate_tenant_seq_no()
""")
# 移除 is_active
op.drop_column('tenant_role_rights', 'is_active')
# 恢復廢棄表不實現downgrade 不支援重建複雜資料)
# op.create_table('employee_identities', ...)
# op.create_table('business_units', ...)