Coverage for src / idx_api / models / user.py: 100%

20 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2025-12-28 11:09 -0700

1"""User model for authentication.""" 

2 

3from datetime import datetime 

4from typing import TYPE_CHECKING, Optional 

5 

6from sqlalchemy import DateTime, ForeignKey, String 

7from sqlalchemy.orm import Mapped, mapped_column, relationship 

8 

9from idx_api.models.base import Base, TimestampMixin 

10 

11if TYPE_CHECKING: 

12 from idx_api.models.agent import Agent 

13 from idx_api.models.api_key import APIKey 

14 from idx_api.models.broker import Broker 

15 from idx_api.models.brokerage import Brokerage 

16 

17 

18class User(Base, TimestampMixin): 

19 """Application user with role-based access control.""" 

20 

21 __tablename__ = "users" 

22 

23 id: Mapped[int] = mapped_column(primary_key=True) 

24 

25 # Authentication 

26 keycloak_user_id: Mapped[Optional[str]] = mapped_column( 

27 String(100), unique=True, index=True 

28 ) # UUID from Keycloak 

29 email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True) 

30 

31 # Authorization - role hierarchy: admin (4) > broker (2) > agent (1) > viewer (0) 

32 role: Mapped[str] = mapped_column( 

33 String(20), nullable=False, default="viewer" 

34 ) # admin, broker, agent, viewer 

35 

36 # Association with brokerage/broker contact/agent 

37 brokerage_id: Mapped[Optional[int]] = mapped_column(ForeignKey("brokerages.id"), index=True) 

38 broker_id: Mapped[Optional[int]] = mapped_column(ForeignKey("brokers.id"), index=True) 

39 agent_id: Mapped[Optional[int]] = mapped_column(ForeignKey("agents.id"), index=True) 

40 

41 # Activity tracking 

42 last_login_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) 

43 

44 # Soft delete 

45 disabled_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) 

46 

47 # Relationships 

48 brokerage: Mapped[Optional["Brokerage"]] = relationship(back_populates="users") 

49 broker_contact: Mapped[Optional["Broker"]] = relationship(back_populates="users") 

50 agent: Mapped[Optional["Agent"]] = relationship(back_populates="users") 

51 api_keys: Mapped[list["APIKey"]] = relationship(back_populates="user") 

52 

53 def __repr__(self) -> str: 

54 return f"<User(id={self.id}, email='{self.email}', role='{self.role}')>"