Files
hr-portal/backend/app/models/permission.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

113 lines
3.3 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.
"""
系統權限 Model
管理員工在各系統的存取權限 (Gitea, Portainer, etc.)
符合設計文件規範: HR Portal設計文件.md
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Index, UniqueConstraint
from sqlalchemy.orm import relationship
from app.db.base import Base
class Permission(Base):
"""系統權限表"""
__tablename__ = "tenant_permissions"
__table_args__ = (
# 同一員工在同一系統只能有一個權限記錄
UniqueConstraint("employee_id", "system_name", name="uq_employee_system"),
# 索引
Index("idx_permissions_employee", "employee_id"),
Index("idx_permissions_tenant", "tenant_id"),
Index("idx_permissions_system", "system_name"),
)
id = Column(Integer, primary_key=True, index=True)
tenant_id = Column(
Integer,
ForeignKey("tenants.id", ondelete="CASCADE"),
nullable=False,
index=True,
comment="租戶 ID"
)
employee_id = Column(
Integer,
ForeignKey("tenant_employees.id", ondelete="CASCADE"),
nullable=False,
index=True,
comment="員工 ID"
)
# 權限設定
system_name = Column(
String(100),
nullable=False,
index=True,
comment="系統名稱 (gitea, portainer, traefik, keycloak)"
)
access_level = Column(
String(50),
default='user',
nullable=False,
comment="存取層級 (admin/user/readonly)"
)
# 授予資訊
granted_at = Column(
DateTime,
default=datetime.utcnow,
nullable=False,
comment="授予時間"
)
granted_by = Column(
Integer,
ForeignKey("tenant_employees.id", ondelete="SET NULL"),
nullable=True,
comment="授予人 (員工 ID)"
)
# 通用欄位 (Note: Permission 表不需要 is_active依靠 granted_at 判斷)
edit_by = Column(String(100), nullable=True, comment="最後編輯者")
created_at = Column(DateTime, default=datetime.utcnow, nullable=False, comment="建立時間")
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False, comment="更新時間")
# 關聯
employee = relationship(
"Employee",
foreign_keys=[employee_id],
back_populates="permissions"
)
granted_by_employee = relationship(
"Employee",
foreign_keys=[granted_by]
)
granter = relationship(
"Employee",
foreign_keys=[granted_by],
viewonly=True,
)
tenant = relationship("Tenant")
def __repr__(self):
return f"<Permission {self.system_name}:{self.access_level}>"
@classmethod
def get_available_systems(cls) -> list[str]:
"""取得可用的系統清單"""
return [
"gitea", # Git 代碼託管
"portainer", # 容器管理
"traefik", # 反向代理管理
"keycloak", # SSO 管理
]
@classmethod
def get_available_access_levels(cls) -> list[str]:
"""取得可用的存取層級"""
return [
"admin", # 管理員 (完整控制)
"user", # 一般使用者
"readonly", # 唯讀
]