Some checks failed
CD Pipeline / deploy (push) Failing after 59s
- 建立 Gitea Actions CD pipeline (.gitea/workflows/cd.yaml) - 部署模式: rsync Python 檔案至 188 → docker restart (volume mount) - Dockerfile/requirements 變動時自動重建 Docker image - 部署通知: Telegram (開始/成功/失敗) - 健康檢查: https://mo.wooo.work/health (最多 5 次重試) - 同步最新 CLAUDE.md / ADR-008 / memory (2026-04-19) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
111 lines
3.6 KiB
Python
111 lines
3.6 KiB
Python
"""
|
||
用戶與登入歷史資料模型
|
||
|
||
提供:
|
||
- User: 用戶帳號表
|
||
- LoginHistory: 登入歷史記錄表
|
||
"""
|
||
|
||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text
|
||
from sqlalchemy.orm import relationship
|
||
from datetime import datetime
|
||
from database.models import Base
|
||
|
||
|
||
class User(Base):
|
||
"""用戶帳號表"""
|
||
__tablename__ = 'users'
|
||
|
||
id = Column(Integer, primary_key=True)
|
||
username = Column(String(50), unique=True, nullable=False, index=True)
|
||
email = Column(String(120), unique=True, nullable=True)
|
||
password_hash = Column(String(256), nullable=False)
|
||
role = Column(String(20), default='user', index=True) # admin, manager, user
|
||
display_name = Column(String(100))
|
||
is_active = Column(Boolean, default=True)
|
||
password_changed_at = Column(DateTime) # 密碼變更時間
|
||
created_at = Column(DateTime, default=datetime.now)
|
||
updated_at = Column(DateTime, onupdate=datetime.now)
|
||
created_by = Column(Integer, ForeignKey('users.id'), nullable=True)
|
||
|
||
# 關聯
|
||
login_history = relationship("LoginHistory", back_populates="user", cascade="all, delete-orphan")
|
||
|
||
# 角色常數
|
||
ROLE_ADMIN = 'admin'
|
||
ROLE_MANAGER = 'manager'
|
||
ROLE_USER = 'user'
|
||
|
||
ROLES = [ROLE_ADMIN, ROLE_MANAGER, ROLE_USER]
|
||
|
||
ROLE_LABELS = {
|
||
'admin': '系統管理員',
|
||
'manager': '管理者',
|
||
'user': '一般用戶'
|
||
}
|
||
|
||
def get_role_label(self):
|
||
"""取得角色顯示名稱"""
|
||
return self.ROLE_LABELS.get(self.role, self.role)
|
||
|
||
def is_admin(self):
|
||
"""是否為管理員"""
|
||
return self.role == self.ROLE_ADMIN
|
||
|
||
def is_manager_or_above(self):
|
||
"""是否為管理者或以上"""
|
||
return self.role in [self.ROLE_ADMIN, self.ROLE_MANAGER]
|
||
|
||
def to_dict(self):
|
||
"""轉換為字典"""
|
||
return {
|
||
'id': self.id,
|
||
'username': self.username,
|
||
'email': self.email,
|
||
'role': self.role,
|
||
'role_label': self.get_role_label(),
|
||
'display_name': self.display_name,
|
||
'is_active': self.is_active,
|
||
'password_changed_at': self.password_changed_at.isoformat() if self.password_changed_at else None,
|
||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
||
}
|
||
|
||
|
||
class LoginHistory(Base):
|
||
"""登入歷史記錄表"""
|
||
__tablename__ = 'login_history'
|
||
|
||
id = Column(Integer, primary_key=True)
|
||
user_id = Column(Integer, ForeignKey('users.id'), nullable=True) # 允許 NULL,記錄失敗的登入嘗試
|
||
username_attempted = Column(String(50)) # 嘗試登入的帳號名稱
|
||
login_time = Column(DateTime, default=datetime.now, index=True)
|
||
ip_address = Column(String(45)) # 支援 IPv6
|
||
user_agent = Column(String(256))
|
||
status = Column(String(20), index=True) # success, failed, locked
|
||
failure_reason = Column(String(100))
|
||
|
||
# 關聯
|
||
user = relationship("User", back_populates="login_history")
|
||
|
||
# 狀態常數
|
||
STATUS_SUCCESS = 'success'
|
||
STATUS_FAILED = 'failed'
|
||
STATUS_LOCKED = 'locked'
|
||
|
||
def to_dict(self):
|
||
"""轉換為字典"""
|
||
return {
|
||
'id': self.id,
|
||
'user_id': self.user_id,
|
||
'username_attempted': self.username_attempted,
|
||
'login_time': self.login_time.isoformat() if self.login_time else None,
|
||
'ip_address': self.ip_address,
|
||
'user_agent': self.user_agent,
|
||
'status': self.status,
|
||
'failure_reason': self.failure_reason,
|
||
}
|
||
|
||
|
||
print("✅ User models 已載入")
|