"""Nutzer, Teams und Web-Sitzungen (→ Konzept 5).""" from datetime import datetime from sqlalchemy import Boolean, ForeignKey, Integer, String, false, true from sqlalchemy.orm import Mapped, mapped_column from app.core.database import Base, now class User(Base): __tablename__ = "users" id: Mapped[int] = mapped_column(primary_key=True) username: Mapped[str] = mapped_column(String(64), unique=True) display_name: Mapped[str] = mapped_column(String(200)) # Globale Administration (Modelle, Connectoren, Policies) is_admin: Mapped[bool] = mapped_column(Boolean, default=False) # Darf Anträge mit Freigabestufe global_approver entscheiden, auch ohne Mitglied des Workspace zu sein is_global_approver: Mapped[bool] = mapped_column(Boolean, default=False, server_default=false()) # Für Benachrichtigungen; notify = Nutzer möchte E-Mails erhalten email: Mapped[str | None] = mapped_column(String(254)) notify: Mapped[bool] = mapped_column(Boolean, default=True, server_default=true()) # Kennung aus dem späteren Identity Provider (OIDC/Keycloak); leer bei Demo-Nutzern external_id: Mapped[str | None] = mapped_column(String(200), unique=True) active: Mapped[bool] = mapped_column(Boolean, default=True) # Eigenes Passwort (Argon2id); leer = nur Demo-Anmeldung bzw. später Keycloak password_hash: Mapped[str | None] = mapped_column(String(200)) # Nach Anlegen oder Zurücksetzen: erst ein eigenes Passwort setzen must_change_password: Mapped[bool] = mapped_column(Boolean, default=False) failed_logins: Mapped[int] = mapped_column(Integer, default=0) locked_until: Mapped[datetime | None] password_changed: Mapped[datetime | None] created: Mapped[datetime] = mapped_column(default=now) class Team(Base): __tablename__ = "teams" id: Mapped[int] = mapped_column(primary_key=True) slug: Mapped[str] = mapped_column(String(64), unique=True) name: Mapped[str] = mapped_column(String(200)) created: Mapped[datetime] = mapped_column(default=now) class TeamMembership(Base): __tablename__ = "team_memberships" team_id: Mapped[int] = mapped_column(ForeignKey("teams.id", ondelete="CASCADE"), primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), primary_key=True) class WebSession(Base): """Serverseitige Sitzung; im Cookie steht nur das Token, hier sein SHA-256.""" __tablename__ = "web_sessions" id: Mapped[str] = mapped_column(String(64), primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) csrf: Mapped[str] = mapped_column(String(64)) created: Mapped[datetime] = mapped_column(default=now) expires_at: Mapped[datetime] revoked_at: Mapped[datetime | None]