"""SQLAlchemy identity and tenant metadata models.""" from __future__ import annotations from datetime import datetime, timezone from typing import List from uuid import uuid4 from sqlalchemy import CheckConstraint, DateTime, Enum as SAEnum, ForeignKey, Integer, String, UniqueConstraint, func, text from sqlalchemy.orm import Mapped, mapped_column, relationship from ..db import Base from ..security.policy import Role def _id(prefix: str) -> str: return f"{prefix}_{uuid4().hex}" def _utc_now() -> datetime: return datetime.now(timezone.utc) def _role_values(enum_type): return [member.value for member in enum_type] class Organization(Base): __tablename__ = "organizations" id: Mapped[str] = mapped_column(String(64), primary_key=True, default=lambda: _id("org")) name: Mapped[str] = mapped_column(String(160), nullable=False) slug: Mapped[str] = mapped_column(String(80), nullable=False, unique=True, index=True) status: Mapped[str] = mapped_column( String(32), nullable=False, default="active", server_default=text("'active'") ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=_utc_now, server_default=func.now(), nullable=False ) memberships: Mapped[List["Membership"]] = relationship( back_populates="organization", cascade="all, delete-orphan" ) class User(Base): __tablename__ = "users" id: Mapped[str] = mapped_column(String(64), primary_key=True, default=lambda: _id("usr")) email_normalized: Mapped[str] = mapped_column(String(320), nullable=False, unique=True, index=True) password_hash: Mapped[str] = mapped_column( String(512), nullable=False, default="!invite_pending", server_default=text("'!invite_pending'") ) status: Mapped[str] = mapped_column( String(32), nullable=False, default="active", server_default=text("'active'") ) auth_version: Mapped[int] = mapped_column( Integer, nullable=False, default=0, server_default=text("0") ) locale: Mapped[str] = mapped_column( String(8), nullable=False, default="th", server_default=text("'th'") ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=_utc_now, server_default=func.now(), nullable=False ) last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) memberships: Mapped[List["Membership"]] = relationship( back_populates="user", cascade="all, delete-orphan" ) sessions: Mapped[List["AuthSession"]] = relationship( back_populates="user", cascade="all, delete-orphan" ) class Membership(Base): __tablename__ = "memberships" __table_args__ = ( UniqueConstraint("user_id", "organization_id", name="uq_membership_user_org"), CheckConstraint( "role IN ('super_admin', 'admin', 'user')", name="ck_membership_role", ), ) id: Mapped[str] = mapped_column(String(64), primary_key=True, default=lambda: _id("mem")) user_id: Mapped[str] = mapped_column( ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True ) organization_id: Mapped[str] = mapped_column( ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True ) role: Mapped[Role] = mapped_column( SAEnum( Role, name="role", values_callable=_role_values, native_enum=False, create_constraint=False, validate_strings=True, ), nullable=False, ) status: Mapped[str] = mapped_column( String(32), nullable=False, default="active", server_default=text("'active'") ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=_utc_now, server_default=func.now(), nullable=False ) user: Mapped[User] = relationship(back_populates="memberships") organization: Mapped[Organization] = relationship(back_populates="memberships") sessions: Mapped[List["AuthSession"]] = relationship(back_populates="membership") class AuthSession(Base): __tablename__ = "sessions" id: Mapped[str] = mapped_column(String(64), primary_key=True, default=lambda: _id("ses")) user_id: Mapped[str] = mapped_column( ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True ) membership_id: Mapped[str] = mapped_column( ForeignKey("memberships.id", ondelete="CASCADE"), nullable=False, index=True ) token_hash: Mapped[str] = mapped_column(String(64), nullable=False, unique=True, index=True) auth_version: Mapped[int] = mapped_column( Integer, nullable=False, default=0, server_default=text("0") ) expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=_utc_now, server_default=func.now(), nullable=False ) last_seen_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) user: Mapped[User] = relationship(back_populates="sessions") membership: Mapped[Membership] = relationship(back_populates="sessions")