from __future__ import annotations import enum from datetime import datetime from typing import Any from sqlalchemy import JSON, Boolean, Column, DateTime, Enum, ForeignKey, Integer, String, Table, Text, func from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship class Base(DeclarativeBase): pass user_roles = Table( "user_roles", Base.metadata, Column("user_id", ForeignKey("users.id"), primary_key=True), Column("role_id", ForeignKey("roles.id"), primary_key=True), ) role_permissions = Table( "role_permissions", Base.metadata, Column("role_id", ForeignKey("roles.id"), primary_key=True), Column("permission_id", ForeignKey("permissions.id"), primary_key=True), ) class Job(Base): __tablename__ = "jobs" id: Mapped[str] = mapped_column(String, primary_key=True) cron_expr: Mapped[str] = mapped_column(String, nullable=False) handler_path: Mapped[str] = mapped_column(String, nullable=False) public_cfg: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) # 密文 token(Fernet 加密后的字符串) secret_cfg: Mapped[str] = mapped_column(Text, default="", nullable=False) enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) last_run_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False ) logs: Mapped[list["JobLog"]] = relationship(back_populates="job", cascade="all, delete-orphan") class JobStatus(str, enum.Enum): RUNNING = "RUNNING" SUCCESS = "SUCCESS" FAILURE = "FAILURE" RETRY = "RETRY" class JobLog(Base): __tablename__ = "job_logs" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) job_id: Mapped[str] = mapped_column(ForeignKey("jobs.id"), index=True, nullable=False) status: Mapped[JobStatus] = mapped_column( Enum(JobStatus, native_enum=False, length=16), nullable=False, ) snapshot_params: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) message: Mapped[str] = mapped_column(Text, default="", nullable=False) traceback: Mapped[str] = mapped_column(Text, default="", nullable=False) # 本次执行期间捕获到的完整运行日志(可能很长,按上层捕获器做截断) run_log: Mapped[str] = mapped_column(Text, default="", nullable=False) celery_task_id: Mapped[str] = mapped_column(String, default="", nullable=False) attempt: Mapped[int] = mapped_column(Integer, default=0, nullable=False) started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) job: Mapped[Job] = relationship(back_populates="logs") class User(Base): __tablename__ = "users" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) username: Mapped[str] = mapped_column(String, unique=True, index=True, nullable=False) password_hash: Mapped[str] = mapped_column(Text, default="", nullable=False) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) is_superuser: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) is_ldap: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False ) roles: Mapped[list["Role"]] = relationship(secondary=user_roles, back_populates="users") class Role(Base): __tablename__ = "roles" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) name: Mapped[str] = mapped_column(String, unique=True, index=True, nullable=False) description: Mapped[str] = mapped_column(Text, default="", nullable=False) users: Mapped[list[User]] = relationship(secondary=user_roles, back_populates="roles") permissions: Mapped[list["Permission"]] = relationship(secondary=role_permissions, back_populates="roles") class Permission(Base): __tablename__ = "permissions" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) code: Mapped[str] = mapped_column(String, unique=True, index=True, nullable=False) description: Mapped[str] = mapped_column(Text, default="", nullable=False) roles: Mapped[list[Role]] = relationship(secondary=role_permissions, back_populates="permissions") class LdapGroup(Base): __tablename__ = "ldap_groups" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) dn: Mapped[str] = mapped_column(String, unique=True, index=True, nullable=False) name: Mapped[str] = mapped_column(String, index=True, nullable=False) class LdapGroupRole(Base): __tablename__ = "ldap_group_roles" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) ldap_group_id: Mapped[int] = mapped_column(ForeignKey("ldap_groups.id"), nullable=False) role_id: Mapped[int] = mapped_column(ForeignKey("roles.id"), nullable=False) class AuditLog(Base): __tablename__ = "audit_logs" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) actor_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True) action: Mapped[str] = mapped_column(String, index=True, nullable=False) target: Mapped[str] = mapped_column(String, default="", nullable=False) detail: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False) ip: Mapped[str] = mapped_column(String, default="", nullable=False) user_agent: Mapped[str] = mapped_column(Text, default="", nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) class Session(Base): __tablename__ = "sessions" id: Mapped[str] = mapped_column(String, primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False, index=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) ip: Mapped[str] = mapped_column(String, default="", nullable=False) user_agent: Mapped[str] = mapped_column(Text, default="", nullable=False)