dak.c2s/backend/app/models/user.py
CCS Admin d09fdccc75 feat: add first_name, last_name, display_name, avatar_url to User model
Add 4 new nullable profile fields to support the upcoming account
management (Kontoverwaltung) feature. Includes Alembic migration
that has been applied to production database.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 09:34:33 +00:00

166 lines
5.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 sqlalchemy.schema import FetchedValue
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(),
server_onupdate=FetchedValue(),
)
# 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 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"),
)