import uuid import enum from datetime import datetime from typing import TYPE_CHECKING from sqlalchemy import Boolean, DateTime, Enum, ForeignKey, String, Text, func from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column, relationship from app.core.database import Base if TYPE_CHECKING: from app.models.company import Company from app.models.department import Department from app.models.session import Session class UserRole(str, enum.Enum): SUPER_ADMIN = "SUPER_ADMIN" COMPANY_ADMIN = "COMPANY_ADMIN" HR = "HR" MANAGER = "MANAGER" EMPLOYEE = "EMPLOYEE" class AuthProvider(str, enum.Enum): LOCAL = "local" LDAP = "ldap" class User(Base): __tablename__ = "users" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) company_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("companies.id", ondelete="CASCADE")) department_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("departments.id", ondelete="SET NULL")) email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True) password_hash: Mapped[str | None] = mapped_column(Text, nullable=True) first_name: Mapped[str] = mapped_column(String(100), nullable=False) last_name: Mapped[str] = mapped_column(String(100), nullable=False) role: Mapped[UserRole] = mapped_column(Enum(UserRole), nullable=False, default=UserRole.EMPLOYEE) auth_provider: Mapped[AuthProvider] = mapped_column( Enum(AuthProvider, name="authprovider", values_callable=lambda x: [e.value for e in x]), nullable=False, default=AuthProvider.LOCAL, ) ldap_dn: Mapped[str | None] = mapped_column(Text) work_schedule_id: Mapped[uuid.UUID | None] = mapped_column( UUID(as_uuid=True), ForeignKey("work_schedules.id", ondelete="SET NULL") ) # Kiosk auth kiosk_pin_hash: Mapped[str | None] = mapped_column(Text) kiosk_qr_token: Mapped[str | None] = mapped_column(Text, unique=True) # Kalender-Kürzel (vom Manager setzbar, für CalDAV-Template $kuerzel) kuerzel: Mapped[str | None] = mapped_column(String(20)) # Personalnummer (numerisch, eindeutig pro Firma; bleibt nach Deaktivierung reserviert) personnel_number: Mapped[str | None] = mapped_column(String(50)) # NFC-UID für Kiosk-Login (optional, eindeutig pro Firma) kiosk_nfc_uid: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True) # TOTP / 2FA totp_secret: Mapped[str | None] = mapped_column(String(64)) totp_enabled: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) # Permissions can_manual_time_entry: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) # Account state is_active: Mapped[bool] = mapped_column(Boolean, default=True) invite_token_hash: Mapped[str | None] = mapped_column(Text) invite_expires: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) last_login: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) # Relationships company: Mapped["Company"] = relationship("Company", back_populates="users") department: Mapped["Department | None"] = relationship( "Department", primaryjoin="User.department_id == Department.id", foreign_keys="[User.department_id]", back_populates="members", ) sessions: Mapped[list["Session"]] = relationship("Session", back_populates="user", cascade="all, delete-orphan", lazy="noload") @property def full_name(self) -> str: return f"{self.first_name} {self.last_name}" def is_admin_or_above(self) -> bool: return self.role in (UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN) def __repr__(self) -> str: return f""