Files
lux_fiscal/backend/app/db/models/user.py
lauadminandClaude Sonnet 5 2664cb8213 Initial commit: backend scaffold, auth, frontend login
- FastAPI + SQLAlchemy async + Alembic + Postgres backend
- Auth: JWT access + rotating refresh tokens, argon2, roles, audit log
- React 19 + Vite frontend: login page, protected route, auth context
- Docker Compose: postgres, redis, migrate, api, worker (placeholder), frontend/nginx

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 15:07:15 +03:00

75 lines
3.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Пользователи, роли и refresh-токены."""
from __future__ import annotations
import enum
import uuid
from datetime import datetime
from sqlalchemy import Boolean, DateTime, Enum, ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
class UserRole(enum.StrEnum):
"""Роли фиксированы кодом: набор прав завязан на фискальную ответственность."""
ADMIN = "admin" # всё, включая админку, кассы и ключи
CASHIER = "cashier" # очередь и пробитие чеков
VIEWER = "viewer" # только чтение
class User(UUIDPrimaryKeyMixin, TimestampMixin, Base):
__tablename__ = "users"
email: Mapped[str] = mapped_column(String(320), unique=True, index=True, nullable=False)
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
full_name: Mapped[str] = mapped_column(String(255), nullable=False)
role: Mapped[UserRole] = mapped_column(
Enum(UserRole, name="user_role", values_callable=lambda e: [i.value for i in e]),
nullable=False,
default=UserRole.CASHIER,
)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
refresh_tokens: Mapped[list[RefreshToken]] = relationship(
back_populates="user", cascade="all, delete-orphan"
)
def __repr__(self) -> str:
return f"<User {self.email} ({self.role.value})>"
class RefreshToken(UUIDPrimaryKeyMixin, TimestampMixin, Base):
"""Выданные refresh-токены.
Храним SHA-256 хеш, а не сам токен: утечка дампа БД не должна давать
возможность войти. Ротация — выдача нового токена с проставлением
`replaced_by_id` у старого, что позволяет обнаружить повторное
использование уже израсходованного токена.
"""
__tablename__ = "refresh_tokens"
user_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("users.id", ondelete="CASCADE"), index=True, nullable=False
)
token_hash: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False)
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
replaced_by_id: Mapped[uuid.UUID | None] = mapped_column(
ForeignKey("refresh_tokens.id", ondelete="SET NULL")
)
user_agent: Mapped[str | None] = mapped_column(String(512))
ip: Mapped[str | None] = mapped_column(String(64))
user: Mapped[User] = relationship(back_populates="refresh_tokens")
@property
def is_active(self) -> bool:
from datetime import UTC
return self.revoked_at is None and self.expires_at > datetime.now(UTC)