|
|
@@ -0,0 +1,85 @@
|
|
|
+"""
|
|
|
+User model - superadmin and organization users.
|
|
|
+"""
|
|
|
+
|
|
|
+from datetime import datetime
|
|
|
+
|
|
|
+from sqlalchemy import Boolean, DateTime, ForeignKey, String
|
|
|
+from sqlalchemy.dialects.postgresql import INET
|
|
|
+from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
+
|
|
|
+from app.models.base import Base
|
|
|
+
|
|
|
+
|
|
|
+class User(Base):
|
|
|
+ """
|
|
|
+ User model for both superadmin and organization users.
|
|
|
+
|
|
|
+ Roles:
|
|
|
+ - superadmin: Full system access (organization_id = NULL)
|
|
|
+ - owner: Organization owner (can manage users)
|
|
|
+ - admin: Full organization access (cannot manage users)
|
|
|
+ - manager: Read-only analytics access
|
|
|
+ - operator: Access to devices and beacons
|
|
|
+ - viewer: Read-only access
|
|
|
+ """
|
|
|
+
|
|
|
+ __tablename__ = "users"
|
|
|
+
|
|
|
+ id: Mapped[int] = mapped_column(primary_key=True)
|
|
|
+ email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
|
|
|
+ hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
|
+ full_name: Mapped[str | None] = mapped_column(String(255))
|
|
|
+ phone: Mapped[str | None] = mapped_column(String(50))
|
|
|
+
|
|
|
+ # Role: superadmin, owner, admin, manager, operator, viewer
|
|
|
+ role: Mapped[str] = mapped_column(String(20), nullable=False)
|
|
|
+
|
|
|
+ # Status: pending, active, suspended, deleted
|
|
|
+ status: Mapped[str] = mapped_column(
|
|
|
+ String(20), default="pending", nullable=False
|
|
|
+ )
|
|
|
+
|
|
|
+ # Organization (NULL for superadmin)
|
|
|
+ organization_id: Mapped[int | None] = mapped_column(
|
|
|
+ ForeignKey("organizations.id", ondelete="CASCADE")
|
|
|
+ )
|
|
|
+
|
|
|
+ # Email verification
|
|
|
+ email_verified: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
|
+ email_verification_token: Mapped[str | None] = mapped_column(String(255))
|
|
|
+ email_verified_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
|
+
|
|
|
+ # Password reset
|
|
|
+ password_reset_token: Mapped[str | None] = mapped_column(String(255))
|
|
|
+ password_reset_expires: Mapped[datetime | None] = mapped_column(
|
|
|
+ DateTime(timezone=True)
|
|
|
+ )
|
|
|
+
|
|
|
+ # Login tracking
|
|
|
+ last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
|
+ last_login_ip: Mapped[str | None] = mapped_column(INET)
|
|
|
+
|
|
|
+ # Relationships
|
|
|
+ organization: Mapped["Organization | None"] = relationship(
|
|
|
+ "Organization", back_populates="users"
|
|
|
+ )
|
|
|
+ refresh_tokens: Mapped[list["RefreshToken"]] = relationship(
|
|
|
+ "RefreshToken", back_populates="user", cascade="all, delete-orphan"
|
|
|
+ )
|
|
|
+ audit_logs: Mapped[list["AuditLog"]] = relationship(
|
|
|
+ "AuditLog", back_populates="user"
|
|
|
+ )
|
|
|
+
|
|
|
+ @property
|
|
|
+ def is_superadmin(self) -> bool:
|
|
|
+ """Check if user is superadmin."""
|
|
|
+ return self.role == "superadmin"
|
|
|
+
|
|
|
+ @property
|
|
|
+ def is_owner(self) -> bool:
|
|
|
+ """Check if user is organization owner."""
|
|
|
+ return self.role == "owner"
|
|
|
+
|
|
|
+ def __repr__(self) -> str:
|
|
|
+ return f"<User {self.id}: {self.email} ({self.role})>"
|