Подготовка к замене AuthRejectReason
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from net.xeaf.rack.models import AccountModel
|
from net.xeaf.rack.models import AccountModel
|
||||||
|
from net.xeaf.rack.utils import DateUtils
|
||||||
|
|
||||||
|
|
||||||
class AccountTestData:
|
class AccountTestData:
|
||||||
@@ -49,6 +50,7 @@ class AccountTestData:
|
|||||||
account.full_name = "Иван И. Иванов"
|
account.full_name = "Иван И. Иванов"
|
||||||
account.email = "i.ivanov@xeaf.net"
|
account.email = "i.ivanov@xeaf.net"
|
||||||
account.set_password(cls.DEFAULT_PASSWORD)
|
account.set_password(cls.DEFAULT_PASSWORD)
|
||||||
|
account.joined_at = DateUtils.now()
|
||||||
account.save()
|
account.save()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -64,5 +66,5 @@ class AccountTestData:
|
|||||||
account.full_name = "Петр П. Петров"
|
account.full_name = "Петр П. Петров"
|
||||||
account.email = "p.petrov@xeaf.net"
|
account.email = "p.petrov@xeaf.net"
|
||||||
account.set_password(cls.DEFAULT_PASSWORD)
|
account.set_password(cls.DEFAULT_PASSWORD)
|
||||||
|
account.joined_at = DateUtils.now()
|
||||||
account.save()
|
account.save()
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,16 @@ class AccountManager:
|
|||||||
Реализует методы работы с Аккаунтами
|
Реализует методы работы с Аккаунтами
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def check_account(cls, username: str) -> None:
|
||||||
|
"""
|
||||||
|
Проверяет Аккаунт с указанным именем
|
||||||
|
"""
|
||||||
|
|
||||||
|
account = AccountModel.objects.filter(username=username).first()
|
||||||
|
if account is None:
|
||||||
|
raise ValueError("Account not found")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def restore_account(cls, account: AccountModel) -> None:
|
def restore_account(cls, account: AccountModel) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class SessionManager[T:SessionModel]:
|
|||||||
DEFAULT_SESSION_TTL = 3600
|
DEFAULT_SESSION_TTL = 3600
|
||||||
""" Время жизни сессии по умолчанию """
|
""" Время жизни сессии по умолчанию """
|
||||||
|
|
||||||
__model_class: Type[T] | None
|
__model_class: Type[T] | None = None
|
||||||
""" Хранилище для класса сессии """
|
""" Хранилище для класса сессии """
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -131,7 +131,7 @@ class SessionManager[T:SessionModel]:
|
|||||||
return model.objects.create(
|
return model.objects.create(
|
||||||
token=CryptoManager.gen_token(),
|
token=CryptoManager.gen_token(),
|
||||||
account=account,
|
account=account,
|
||||||
actual_account=actual_account
|
actual_account=actual_account,
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 6.0.7 on 2026-07-13 10:28
|
# Generated by Django 6.0.7 on 2026-07-14 09:26
|
||||||
|
|
||||||
import django.contrib.auth.models
|
import django.contrib.auth.models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
@@ -51,7 +51,7 @@ class Migration(migrations.Migration):
|
|||||||
name='SessionModel',
|
name='SessionModel',
|
||||||
fields=[
|
fields=[
|
||||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='rack.mixin.created_at')),
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='rack.mixin.created_at')),
|
||||||
('expires_at', models.DateTimeField(verbose_name='rack.mixin.expires_at')),
|
('expires_at', models.DateTimeField(blank=True, null=True, verbose_name='rack.mixin.expires_at')),
|
||||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='rack.mixin.id')),
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='rack.mixin.id')),
|
||||||
('token', models.CharField(max_length=255, verbose_name='rack.models.session_model.token')),
|
('token', models.CharField(max_length=255, verbose_name='rack.models.session_model.token')),
|
||||||
('device_id', models.CharField(blank=True, default='WEB', max_length=36, null=True, verbose_name='rack.models.session_model.device_id')),
|
('device_id', models.CharField(blank=True, default='WEB', max_length=36, null=True, verbose_name='rack.models.session_model.device_id')),
|
||||||
|
|||||||
@@ -9,9 +9,10 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from net.xeaf.rack.utils import DateUtils
|
||||||
|
|
||||||
|
|
||||||
class DeletedAtMixin(models.Model):
|
class DeletedAtMixin(models.Model):
|
||||||
"""
|
"""
|
||||||
@@ -33,7 +34,7 @@ class DeletedAtMixin(models.Model):
|
|||||||
:param save: Признак необходимости сохранения
|
:param save: Признак необходимости сохранения
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.deleted_at = timezone.now()
|
self.deleted_at = DateUtils.now()
|
||||||
if save:
|
if save:
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,11 @@
|
|||||||
Описание класса ExpiredAtMixin
|
Описание класса ExpiredAtMixin
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from net.xeaf.rack.utils import DateUtils
|
||||||
|
|
||||||
|
|
||||||
class ExpiresAtMixin(models.Model):
|
class ExpiresAtMixin(models.Model):
|
||||||
"""
|
"""
|
||||||
@@ -21,8 +20,8 @@ class ExpiresAtMixin(models.Model):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
expires_at = models.DateTimeField(
|
expires_at = models.DateTimeField(
|
||||||
null=False,
|
null=True,
|
||||||
blank=False,
|
blank=True,
|
||||||
editable=True,
|
editable=True,
|
||||||
verbose_name=_("rack.mixin.expires_at"),
|
verbose_name=_("rack.mixin.expires_at"),
|
||||||
)
|
)
|
||||||
@@ -40,7 +39,7 @@ class ExpiresAtMixin(models.Model):
|
|||||||
if seconds <= 0:
|
if seconds <= 0:
|
||||||
raise ValueError(f"The seconds parameter must be greater than 0")
|
raise ValueError(f"The seconds parameter must be greater than 0")
|
||||||
|
|
||||||
self.expires_at = timezone.now() + timedelta(seconds=seconds)
|
self.expires_at = DateUtils.shift_time(seconds=seconds)
|
||||||
if save:
|
if save:
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@@ -51,7 +50,7 @@ class ExpiresAtMixin(models.Model):
|
|||||||
:param save: Признак необходимости сохранения
|
:param save: Признак необходимости сохранения
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.expires_at = timezone.now()
|
self.expires_at = DateUtils.now()
|
||||||
if save:
|
if save:
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@@ -62,7 +61,11 @@ class ExpiresAtMixin(models.Model):
|
|||||||
:return: Признак просроченного значения
|
:return: Признак просроченного значения
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.expires_at < timezone.now()
|
# Если None - то значение не просрочено
|
||||||
|
if self.expires_at is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.expires_at < DateUtils.now()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -11,9 +11,10 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from net.xeaf.rack.utils import DateUtils
|
||||||
|
|
||||||
|
|
||||||
class UpdatedAtMixin(models.Model):
|
class UpdatedAtMixin(models.Model):
|
||||||
"""
|
"""
|
||||||
@@ -57,7 +58,7 @@ class UpdatedAtMixin(models.Model):
|
|||||||
:param save: Признак необходимости сохранения
|
:param save: Признак необходимости сохранения
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.updated_at = timezone.now()
|
self.updated_at = DateUtils.now()
|
||||||
if save:
|
if save:
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
|||||||
@@ -11,4 +11,4 @@
|
|||||||
from .configuration_manager_tests import ConfigurationManagerTests
|
from .configuration_manager_tests import ConfigurationManagerTests
|
||||||
from .crypto_manager_tests import CryptoManagerTests
|
from .crypto_manager_tests import CryptoManagerTests
|
||||||
from .locale_manager_tests import LocaleManagerTests
|
from .locale_manager_tests import LocaleManagerTests
|
||||||
# from .sesson_manager_tests import SessionManagerTests
|
from .sesson_manager_tests import SessionManagerTests
|
||||||
|
|||||||
99
net/xeaf/rack/tests/utils/date_utils_tests.py
Normal file
99
net/xeaf/rack/tests/utils/date_utils_tests.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса DateUtilsTest
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.test import SimpleTestCase
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from net.xeaf.rack.utils import DateUtils
|
||||||
|
|
||||||
|
|
||||||
|
class DateUtilsTestCase(SimpleTestCase):
|
||||||
|
"""
|
||||||
|
Классы для DateUtils.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_now_returns_frozen_current_time(self) -> None:
|
||||||
|
"""
|
||||||
|
Должен вернуть точно зафиксированное текущее время через mock.patch
|
||||||
|
"""
|
||||||
|
|
||||||
|
fixed_time = datetime.datetime(2026, 7, 14, 12, 0, 0, tzinfo=timezone.get_current_timezone())
|
||||||
|
|
||||||
|
with patch("django.utils.timezone.now", return_value=fixed_time):
|
||||||
|
result = DateUtils.now()
|
||||||
|
self.assertEqual(result, fixed_time)
|
||||||
|
|
||||||
|
def test_now_returns_timezone_aware_datetime(self) -> None:
|
||||||
|
"""
|
||||||
|
Должен возвращать объект datetime с информацией о часовом поясе (is_aware)
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = DateUtils.now()
|
||||||
|
self.assertTrue(timezone.is_aware(result))
|
||||||
|
|
||||||
|
def test_shift_time_uses_now_if_date_time_is_none(self) -> None:
|
||||||
|
"""
|
||||||
|
Должен автоматически взять текущее время (now), если date_time передан как None
|
||||||
|
"""
|
||||||
|
|
||||||
|
fixed_now = datetime.datetime(2026, 7, 14, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||||
|
expected_time = datetime.datetime(2026, 7, 17, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||||
|
|
||||||
|
with patch("django.utils.timezone.now", return_value=fixed_now):
|
||||||
|
result = DateUtils.shift_time(date_time=None, days=3)
|
||||||
|
self.assertEqual(result, expected_time)
|
||||||
|
|
||||||
|
def test_shift_time_uses_now_by_omission(self) -> None:
|
||||||
|
"""
|
||||||
|
Должен автоматически взять текущее время (now), если параметр date_time вообще опущен
|
||||||
|
"""
|
||||||
|
|
||||||
|
fixed_now = datetime.datetime(2026, 7, 14, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||||
|
expected_time = datetime.datetime(2026, 7, 14, 14, 0, 0, tzinfo=datetime.timezone.utc)
|
||||||
|
|
||||||
|
with patch("django.utils.timezone.now", return_value=fixed_now):
|
||||||
|
result = DateUtils.shift_time(hours=2)
|
||||||
|
self.assertEqual(result, expected_time)
|
||||||
|
|
||||||
|
def test_shift_time_adds_days_correctly_with_explicit_date(self) -> None:
|
||||||
|
"""
|
||||||
|
Должен корректно сдвигать время вперед, если передан явный объект datetime
|
||||||
|
"""
|
||||||
|
|
||||||
|
base_time = datetime.datetime(2026, 7, 14, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||||
|
expected_time = datetime.datetime(2026, 7, 19, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||||
|
|
||||||
|
result = DateUtils.shift_time(base_time, days=5)
|
||||||
|
self.assertEqual(result, expected_time)
|
||||||
|
|
||||||
|
def test_shift_time_subtracts_hours_correctly(self) -> None:
|
||||||
|
"""
|
||||||
|
Должен корректно сдвигать время назад при передаче отрицательных значений
|
||||||
|
"""
|
||||||
|
|
||||||
|
base_time = datetime.datetime(2026, 7, 14, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||||
|
expected_time = datetime.datetime(2026, 7, 14, 9, 0, 0, tzinfo=datetime.timezone.utc)
|
||||||
|
|
||||||
|
result = DateUtils.shift_time(base_time, hours=-3)
|
||||||
|
self.assertEqual(result, expected_time)
|
||||||
|
|
||||||
|
def test_shift_time_complex_modification(self) -> None:
|
||||||
|
"""
|
||||||
|
Должен атомарно применять комбинированный сдвиг по всем осям времени
|
||||||
|
"""
|
||||||
|
|
||||||
|
base_time = datetime.datetime(2026, 7, 14, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||||
|
expected_time = datetime.datetime(2026, 7, 15, 14, 30, 15, tzinfo=datetime.timezone.utc)
|
||||||
|
|
||||||
|
result = DateUtils.shift_time(base_time, days=1, hours=2, minutes=30, seconds=15)
|
||||||
|
self.assertEqual(result, expected_time)
|
||||||
@@ -8,3 +8,4 @@
|
|||||||
Пакет служебных классов и функций
|
Пакет служебных классов и функций
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from .date_utils import DateUtils
|
||||||
|
|||||||
9
net/xeaf/rack/utils/auth/__init__.py
Normal file
9
net/xeaf/rack/utils/auth/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Классов авторизации
|
||||||
|
"""
|
||||||
61
net/xeaf/rack/utils/auth/expiring_token_authentication.py
Normal file
61
net/xeaf/rack/utils/auth/expiring_token_authentication.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса ExpiringTokenAuthentication
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from rest_framework.authentication import TokenAuthentication
|
||||||
|
from rest_framework.request import Request
|
||||||
|
|
||||||
|
from net.xeaf.rack.managers import LocaleManager
|
||||||
|
from net.xeaf.rack.managers import SessionManager
|
||||||
|
from net.xeaf.rack.models import AccountModel
|
||||||
|
from net.xeaf.rack.models import SessionModel
|
||||||
|
|
||||||
|
|
||||||
|
class ExpiringTokenAuthentication(TokenAuthentication):
|
||||||
|
"""
|
||||||
|
Реализует механизм авторизации для Токенов со сроком действия
|
||||||
|
"""
|
||||||
|
|
||||||
|
AUTH_TOKEN_PARAM = "x-auth-token"
|
||||||
|
""" Идентификатор параметра токена """
|
||||||
|
|
||||||
|
def authenticate(self, request: Request) -> tuple[AccountModel | None, CoreSessionModel | None] | tuple[Any, Any] | None:
|
||||||
|
"""
|
||||||
|
Метод авторизации
|
||||||
|
|
||||||
|
:param request: Информация о запросе
|
||||||
|
"""
|
||||||
|
|
||||||
|
LocaleManager.adjust_request_language(request)
|
||||||
|
|
||||||
|
key = request.query_params.get(self.AUTH_TOKEN_PARAM, None)
|
||||||
|
if key is not None:
|
||||||
|
return self.authenticate_credentials(key)
|
||||||
|
|
||||||
|
return super().authenticate(request)
|
||||||
|
|
||||||
|
def authenticate_credentials(self, key: str) -> tuple[AccountModel | None, CoreSessionModel | None]:
|
||||||
|
"""
|
||||||
|
Проверка Токена авторизации
|
||||||
|
|
||||||
|
:param key: Токен авторизации
|
||||||
|
"""
|
||||||
|
|
||||||
|
session = SessionManager[SessionModel].find_by_token(token=key)
|
||||||
|
if not session or session.is_expired():
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
account = session.account
|
||||||
|
if not account.is_active or account.is_deleted():
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
SessionManager.renew_session(session)
|
||||||
|
return session.account, session
|
||||||
46
net/xeaf/rack/utils/date_utils.py
Normal file
46
net/xeaf/rack/utils/date_utils.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# DRF Rack
|
||||||
|
# Библиотека классов расширений для Django REST Framework
|
||||||
|
#
|
||||||
|
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||||
|
# Все права защищены. Лицензия: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Описание класса DateUtils
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
|
class DateUtils:
|
||||||
|
"""
|
||||||
|
Реализует функции работы с датами и временем
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def now(cls) -> datetime.datetime:
|
||||||
|
"""
|
||||||
|
Возвращает текущую дату и время
|
||||||
|
"""
|
||||||
|
|
||||||
|
return timezone.now()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def shift_time(cls, date_time: datetime.datetime | None = None, days: int = 0, hours: int = 0, minutes: int = 0, seconds: int = 0) -> datetime.datetime:
|
||||||
|
"""
|
||||||
|
Добавляет к дате и времени заданное количество дней, часов, минут и секунд
|
||||||
|
|
||||||
|
:param date_time: Дата и время
|
||||||
|
:param days: Количество дней
|
||||||
|
:param hours: Количество часов
|
||||||
|
:param minutes: Количество минут
|
||||||
|
:param seconds: Количество секунд
|
||||||
|
|
||||||
|
:return: Дата и время с добавленным временем
|
||||||
|
"""
|
||||||
|
|
||||||
|
if date_time is None:
|
||||||
|
date_time = cls.now()
|
||||||
|
|
||||||
|
return date_time + datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
|
||||||
Reference in New Issue
Block a user