Files
leocrm/app/models/user.py
T

72 lines
2.9 KiB
Python

"""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()
)