"""User, authentication, and access control models.""" from __future__ import annotations from datetime import datetime from typing import Optional from sqlalchemy import ( Boolean, CheckConstraint, DateTime, ForeignKey, Index, Integer, String, UniqueConstraint, func, ) from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database import Base class User(Base): __tablename__ = "users" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) username: Mapped[str] = mapped_column(String(100), nullable=False) email: Mapped[str] = mapped_column(String(255), nullable=False) first_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) last_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) display_name: Mapped[Optional[str]] = mapped_column(String(200), nullable=True) avatar_url: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) password_hash: Mapped[str] = mapped_column(String(255), nullable=False) role: Mapped[str] = mapped_column( String(20), nullable=False, server_default="dak_mitarbeiter" ) mfa_secret: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) mfa_enabled: Mapped[bool] = mapped_column( Boolean, nullable=False, server_default="0" ) is_active: Mapped[bool] = mapped_column( Boolean, nullable=False, server_default="1" ) must_change_password: Mapped[bool] = mapped_column( Boolean, nullable=False, server_default="0" ) last_login: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) failed_login_attempts: Mapped[int] = mapped_column( Integer, nullable=False, server_default="0" ) locked_until: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime, nullable=False, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime, nullable=False, server_default=func.now(), onupdate=func.now(), ) # Relationships refresh_tokens: Mapped[list[RefreshToken]] = relationship( back_populates="user", cascade="all, delete-orphan" ) invitations_created: Mapped[list[InvitationLink]] = relationship( foreign_keys="InvitationLink.created_by", back_populates="creator" ) invitations_used: Mapped[list[InvitationLink]] = relationship( foreign_keys="InvitationLink.used_by", back_populates="used_by_user" ) __table_args__ = ( UniqueConstraint("username", name="uk_username"), UniqueConstraint("email", name="uk_email"), CheckConstraint( "role IN ('admin', 'dak_mitarbeiter')", name="chk_role" ), ) class RefreshToken(Base): __tablename__ = "refresh_tokens" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) user_id: Mapped[int] = mapped_column( Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, ) token_hash: Mapped[str] = mapped_column(String(255), nullable=False) expires_at: Mapped[datetime] = mapped_column(DateTime, nullable=False) revoked: Mapped[bool] = mapped_column( Boolean, nullable=False, server_default="0" ) created_at: Mapped[datetime] = mapped_column( DateTime, nullable=False, server_default=func.now() ) # Relationships user: Mapped[User] = relationship(back_populates="refresh_tokens") __table_args__ = ( Index("idx_user", "user_id"), Index("idx_token", "token_hash"), ) class InvitationLink(Base): __tablename__ = "invitation_links" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) token: Mapped[str] = mapped_column(String(255), nullable=False) email: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) role: Mapped[str] = mapped_column( String(20), nullable=False, server_default="dak_mitarbeiter" ) created_by: Mapped[Optional[int]] = mapped_column( Integer, ForeignKey("users.id"), nullable=True ) expires_at: Mapped[datetime] = mapped_column(DateTime, nullable=False) used_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) used_by: Mapped[Optional[int]] = mapped_column( Integer, ForeignKey("users.id"), nullable=True ) is_active: Mapped[bool] = mapped_column( Boolean, nullable=False, server_default="1" ) created_at: Mapped[datetime] = mapped_column( DateTime, nullable=False, server_default=func.now() ) # Relationships creator: Mapped[Optional[User]] = relationship( foreign_keys=[created_by], back_populates="invitations_created" ) used_by_user: Mapped[Optional[User]] = relationship( foreign_keys=[used_by], back_populates="invitations_used" ) __table_args__ = ( UniqueConstraint("token", name="uk_token"), ) class PasswordResetToken(Base): __tablename__ = "password_reset_tokens" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) user_id: Mapped[int] = mapped_column( Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, ) token_hash: Mapped[str] = mapped_column(String(255), nullable=False) expires_at: Mapped[datetime] = mapped_column(DateTime, nullable=False) used_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime, nullable=False, server_default=func.now() ) user: Mapped[User] = relationship() __table_args__ = ( Index("idx_prt_token", "token_hash"), Index("idx_prt_user", "user_id"), ) class FilterPreset(Base): __tablename__ = "filter_presets" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) user_id: Mapped[int] = mapped_column( Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, ) name: Mapped[str] = mapped_column(String(100), nullable=False) filters: Mapped[str] = mapped_column(String(1000), nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime, nullable=False, server_default=func.now() ) user: Mapped[User] = relationship() __table_args__ = ( Index("idx_fp_user", "user_id"), ) class AllowedDomain(Base): __tablename__ = "allowed_domains" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) domain: Mapped[str] = mapped_column(String(255), nullable=False) role: Mapped[str] = mapped_column( String(20), nullable=False, server_default="dak_mitarbeiter" ) is_active: Mapped[bool] = mapped_column( Boolean, nullable=False, server_default="1" ) created_at: Mapped[datetime] = mapped_column( DateTime, nullable=False, server_default=func.now() ) __table_args__ = ( UniqueConstraint("domain", name="uk_domain"), )