Corp/backend/api/auth/models.py

278 lines
8.2 KiB
Python

from __future__ import annotations
import uuid
from django.conf import settings
from django.db import models
from django.utils import timezone
class UserAuthSession(models.Model):
session_id = models.UUIDField(
default=uuid.uuid4,
editable=False,
unique=True,
db_index=True,
verbose_name="Session ID",
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="auth_sessions",
verbose_name="Пользователь",
)
refresh_token_hash = models.CharField(
max_length=128,
blank=True,
default="",
verbose_name="Хеш refresh token",
)
ip_address = models.GenericIPAddressField(
null=True,
blank=True,
verbose_name="IP адрес",
)
user_agent = models.TextField(blank=True, default="", verbose_name="User-Agent")
device_name = models.CharField(
max_length=255,
blank=True,
default="",
verbose_name="Устройство",
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Создана")
last_seen_at = models.DateTimeField(
auto_now_add=True,
verbose_name="Последняя активность",
)
revoked_at = models.DateTimeField(
null=True,
blank=True,
db_index=True,
verbose_name="Отозвана",
)
revoked_reason = models.CharField(
max_length=64,
blank=True,
default="",
verbose_name="Причина отзыва",
)
class Meta:
ordering = ["-last_seen_at", "-created_at"]
indexes = [
models.Index(fields=["user", "revoked_at"]),
models.Index(fields=["user", "-last_seen_at"]),
]
verbose_name = "Сессия аутентификации"
verbose_name_plural = "Сессии аутентификации"
def __str__(self) -> str:
return f"{self.user_id}:{self.session_id}"
@property
def is_revoked(self) -> bool:
return self.revoked_at is not None
def revoke(self, *, reason: str = "manual", commit: bool = True) -> None:
if self.revoked_at:
return
self.revoked_at = timezone.now()
self.revoked_reason = reason
if commit:
self.save(update_fields=["revoked_at", "revoked_reason"])
from .realtime import schedule_auth_session_revoked
schedule_auth_session_revoked(
user_id=self.user_id,
session_id=str(self.session_id),
)
class QrLoginToken(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="qr_login_tokens",
verbose_name="Пользователь",
)
token_hash = models.CharField(
max_length=128,
unique=True,
db_index=True,
verbose_name="Хеш QR-токена",
)
created_by_session = models.ForeignKey(
UserAuthSession,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="created_qr_login_tokens",
verbose_name="Сессия-источник",
)
used_session = models.ForeignKey(
UserAuthSession,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="used_qr_login_tokens",
verbose_name="Созданная сессия",
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Создан")
expires_at = models.DateTimeField(db_index=True, verbose_name="Истекает")
used_at = models.DateTimeField(
null=True,
blank=True,
db_index=True,
verbose_name="Использован",
)
created_ip_address = models.GenericIPAddressField(
null=True,
blank=True,
verbose_name="IP создания",
)
created_user_agent = models.TextField(
blank=True,
default="",
verbose_name="User-Agent создания",
)
used_ip_address = models.GenericIPAddressField(
null=True,
blank=True,
verbose_name="IP использования",
)
used_user_agent = models.TextField(
blank=True,
default="",
verbose_name="User-Agent использования",
)
class Meta:
ordering = ["-created_at"]
indexes = [
models.Index(fields=["user", "used_at", "expires_at"]),
]
verbose_name = "QR-токен входа"
verbose_name_plural = "QR-токены входа"
def __str__(self) -> str:
return f"{self.user_id}:{self.created_at:%Y-%m-%d %H:%M:%S}"
@property
def is_used(self) -> bool:
return self.used_at is not None
@property
def is_expired(self) -> bool:
return timezone.now() >= self.expires_at
class QrLoginRequest(models.Model):
scan_token_hash = models.CharField(
max_length=128,
unique=True,
db_index=True,
verbose_name="Хеш токена сканирования",
)
client_secret_hash = models.CharField(
max_length=128,
unique=True,
db_index=True,
verbose_name="Хеш секрета клиента",
)
requester_ip_address = models.GenericIPAddressField(
null=True,
blank=True,
verbose_name="IP запрашивающего устройства",
)
requester_user_agent = models.TextField(
blank=True,
default="",
verbose_name="User-Agent запрашивающего устройства",
)
requester_device_name = models.CharField(
max_length=255,
blank=True,
default="",
verbose_name="Запрашивающее устройство",
)
approved_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.CASCADE,
related_name="approved_qr_login_requests",
verbose_name="Подтвердил пользователь",
)
approved_by_session = models.ForeignKey(
UserAuthSession,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="approved_qr_login_requests",
verbose_name="Подтвердила сессия",
)
used_session = models.ForeignKey(
UserAuthSession,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="qr_login_requests",
verbose_name="Созданная сессия",
)
denied_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="denied_qr_login_requests",
verbose_name="Отклонил пользователь",
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Создан")
expires_at = models.DateTimeField(db_index=True, verbose_name="Истекает")
approved_at = models.DateTimeField(
null=True,
blank=True,
db_index=True,
verbose_name="Подтвержден",
)
denied_at = models.DateTimeField(
null=True,
blank=True,
db_index=True,
verbose_name="Отклонен",
)
claimed_at = models.DateTimeField(
null=True,
blank=True,
db_index=True,
verbose_name="Получен клиентом",
)
class Meta:
ordering = ["-created_at"]
indexes = [
models.Index(fields=["expires_at", "approved_at", "denied_at"]),
]
verbose_name = "QR-запрос входа"
verbose_name_plural = "QR-запросы входа"
def __str__(self) -> str:
device = self.requester_device_name or "unknown"
return f"{device}:{self.created_at:%Y-%m-%d %H:%M:%S}"
@property
def is_expired(self) -> bool:
return timezone.now() >= self.expires_at
@property
def status(self) -> str:
if self.claimed_at is not None:
return "claimed"
if self.denied_at is not None:
return "denied"
if self.is_expired:
return "expired"
if self.approved_at is not None:
return "approved"
return "pending"