mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-17 21:53:41 +00:00
PostgreSQL requires globally unique index names (unlike MySQL which scopes them per table). Prefix generic names: idx_user→idx_al_user/idx_rt_user, idx_token→idx_rt_token, idx_case/idx_code/idx_haupt→idx_icd_*. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
210 lines
7 KiB
Python
210 lines
7 KiB
Python
"""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_rt_user", "user_id"),
|
|
Index("idx_rt_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"),
|
|
)
|