optional short-TTL cache for the auth user row
TadrisinoTokenAuthentication ran one SELECT on accounts_user for every
authenticated request. Off by default (AUTH_USER_CACHE_TTL=0) so behaviour
is unchanged until a deployment opts in.
accounts/authentication.py
- _get_request_user(): when AUTH_USER_CACHE_TTL>0, cache all concrete
field values under auth:user:<id> and rebuild a full User instance
(so save(update_fields=[...]) stays safe); otherwise load from DB as
before. invalidate_user_cache() / _load_user_or_fail() helpers.
accounts/receivers.py (new)
- post_save/post_delete on User -> invalidate_user_cache(pk); covers
every in-app write path (SetUserGrade, login, refresh, last_activity).
accounts/apps.py
- AccountsConfig.ready() imports receivers.
accounts/tests.py
- AuthUserRowCacheTests: disabled -> loads every request; enabled ->
loads once then cache; User.save() invalidates; SetUserGradeView still
persists and the next request sees the new grade.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from django.conf import settings
|
||||
from django.core import signing
|
||||
from django.core.cache import cache
|
||||
from rest_framework.authentication import BaseAuthentication
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
|
||||
@@ -7,6 +8,48 @@ from .models import User
|
||||
|
||||
ACCESS_TOKEN_SALT = 'tadrisino-access-token'
|
||||
|
||||
# فیلدهای غیرِرابطهایِ User که کش میشن؛ همهی concrete fieldها تا بازسازیِ
|
||||
# instance برای save(update_fields=...) امن بمونه.
|
||||
_USER_CACHE_FIELDS = [f.attname for f in User._meta.concrete_fields]
|
||||
|
||||
|
||||
def _auth_user_cache_key(user_id: int) -> str:
|
||||
return f'auth:user:{user_id}'
|
||||
|
||||
|
||||
def invalidate_user_cache(user_id: int) -> None:
|
||||
cache.delete(_auth_user_cache_key(user_id))
|
||||
|
||||
|
||||
def _load_user_or_fail(user_id: int) -> User:
|
||||
try:
|
||||
return User.objects.get(pk=user_id)
|
||||
except User.DoesNotExist:
|
||||
raise AuthenticationFailed('کاربر پیدا نشد.')
|
||||
|
||||
|
||||
def _get_request_user(user_id: int) -> User:
|
||||
"""
|
||||
ردیفِ کاربر برای request جاری. با AUTH_USER_CACHE_TTL>0 از کش خونده
|
||||
میشه (یک SELECT کمتر در هر request authenticated). instanceِ بازسازیشده
|
||||
کاملِ همهی فیلدهاست، پس save(update_fields=[...]) روش امنه.
|
||||
"""
|
||||
ttl = getattr(settings, 'AUTH_USER_CACHE_TTL', 0)
|
||||
if not ttl:
|
||||
return _load_user_or_fail(user_id)
|
||||
|
||||
key = _auth_user_cache_key(user_id)
|
||||
data = cache.get(key)
|
||||
if data is None:
|
||||
user = _load_user_or_fail(user_id)
|
||||
cache.set(key, {f: getattr(user, f) for f in _USER_CACHE_FIELDS}, ttl)
|
||||
return user
|
||||
|
||||
user = User(**data)
|
||||
user._state.adding = False
|
||||
user._state.db = 'default'
|
||||
return user
|
||||
|
||||
|
||||
def issue_access_token(user_id: int) -> str:
|
||||
"""
|
||||
@@ -48,13 +91,7 @@ class TadrisinoTokenAuthentication(BaseAuthentication):
|
||||
|
||||
token = parts[1]
|
||||
user_id = decode_access_token(token)
|
||||
|
||||
try:
|
||||
user = User.objects.get(pk=user_id)
|
||||
except User.DoesNotExist:
|
||||
raise AuthenticationFailed('کاربر پیدا نشد.')
|
||||
|
||||
return (user, token)
|
||||
return (_get_request_user(user_id), token)
|
||||
|
||||
def authenticate_header(self, request):
|
||||
return self.keyword
|
||||
|
||||
Reference in New Issue
Block a user