"""User and UserTenant models.""" from __future__ import annotations import uuid from datetime import datetime from typing import Any from sqlalchemy import Boolean, DateTime, ForeignKey, String, func from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import UUID as PGUUID from sqlalchemy.orm import Mapped, mapped_column from app.core.db import Base, SoftDeleteMixin, TimestampMixin class User(Base, TimestampMixin, SoftDeleteMixin): """User entity — globally unique email, tenant membership via UserTenant.""" __tablename__ = "users" id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True, index=True) name: Mapped[str] = mapped_column(String(200), nullable=False) first_name: Mapped[str | None] = mapped_column(String(100), nullable=True) last_name: Mapped[str | None] = mapped_column(String(100), nullable=True) avatar_url: Mapped[str | None] = mapped_column(String(500), nullable=True) password_hash: Mapped[str] = mapped_column(String(255), nullable=False) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) preferences: Mapped[dict[str, Any]] = mapped_column(JSONB, default=dict, nullable=False) is_system_admin: Mapped[bool] = mapped_column( Boolean, default=False, nullable=False, server_default="false" ) class UserTenant(Base): """N:M association — user membership in tenants. Single source of truth for tenant membership and role assignment. ``role`` is a built-in role string (admin/editor/viewer). ``role_id`` links to a custom Role record for granular RBAC. ``status`` tracks membership lifecycle (active/invited/disabled). """ __tablename__ = "user_tenants" user_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), primary_key=True ) tenant_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), primary_key=True ) is_default: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) role: Mapped[str] = mapped_column(String(50), nullable=False, default="viewer") role_id: Mapped[uuid.UUID | None] = mapped_column( PGUUID(as_uuid=True), ForeignKey("roles.id", ondelete="SET NULL"), nullable=True, index=True, ) status: Mapped[str] = mapped_column( String(20), nullable=False, default="active", server_default="active" ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() )