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

52 lines
2.1 KiB
Python

"""
用戶郵件別名 Model
支援員工在不同網域擁有多個郵件地址
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, UniqueConstraint, Index
from sqlalchemy.orm import relationship
from app.db.base import Base
class UserEmailAlias(Base):
"""用戶郵件別名表 (一個用戶可以有多個郵件地址)"""
__tablename__ = "user_email_aliases"
__table_args__ = (
# 每個用戶只能有一個主要郵件
Index("idx_user_primary_email", "user_id", unique=True, postgresql_where=Column("is_primary") == True),
# 一般索引
Index("idx_email_aliases_user", "user_id"),
Index("idx_email_aliases_tenant", "tenant_id"),
Index("idx_email_aliases_domain", "domain_id"),
)
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("employees.id", ondelete="CASCADE"), nullable=False, index=True)
tenant_id = Column(Integer, ForeignKey("organizes.id", ondelete="CASCADE"), nullable=False, index=True)
domain_id = Column(Integer, ForeignKey("tenant_domains.id", ondelete="CASCADE"), nullable=False, index=True)
email = Column(String(150), unique=True, nullable=False, index=True, comment="郵件地址 (sales@brand-a.com)")
is_primary = Column(Boolean, default=False, nullable=False, comment="是否為主要郵件地址")
# 時間記錄
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
# 關聯
user = relationship("Employee", back_populates="email_aliases")
domain = relationship("TenantDomain", back_populates="email_aliases")
def __repr__(self):
return f"<UserEmailAlias {self.email} ({'主要' if self.is_primary else '別名'})>"
@property
def local_part(self) -> str:
"""郵件前綴 (@ 之前的部分)"""
return self.email.split('@')[0] if '@' in self.email else self.email
@property
def domain_part(self) -> str:
"""網域部分 (@ 之後的部分)"""
return self.email.split('@')[1] if '@' in self.email else ""