Files
MathApp/accounts/models.py
Zareian 663252a260 Remove the guest system entirely: no is_guest/device_id, no guest throttles, phone login is the only way in
accounts.0008 drops the columns and refuses to run while guest rows exist.
Docs describe the current state; tests that only proved guest behaviour are gone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 18:20:10 +03:30

87 lines
4.5 KiB
Python
Raw 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.
import hashlib
from django.db import models
class User(models.Model):
"""
کاربر اصلی سیستم: ثبت‌نام‌شده با شماره‌ی موبایل (تنها راه ورود).
توجه: این مدل، AUTH_USER_MODEL جنگو نیست؛ یک مدل بیزنسی مستقله که
با TadrisinoTokenAuthentication سفارشی احراز هویت می‌شه.
grade_id فقط یک ارجاع سبک (بدون FK واقعی) به content.Grade هست؛
این اپ عمداً هیچ‌وقت FK واقعی به مدل اپ دیگه نمی‌زنه.
"""
phone = models.CharField(max_length=15, unique=True, null=True, blank=True, db_index=True)
name = models.CharField(max_length=100, blank=True)
avatar = models.CharField(max_length=100, blank=True)
grade_id = models.IntegerField(null=True, blank=True) # ارجاع سبک به content.Grade
created_at = models.DateTimeField(auto_now_add=True)
# آخرین زمانِ فعالیتِ کاربر (لاگین موفق یا رفرش توکن موفق). فقط برای هدف‌گیریِ آینده‌ی کمپین‌های پیامی نگه‌داری
# می‌شه؛ در حال حاضر هیچ منطقِ دیگه‌ای به این فیلد وابسته نیست.
last_activity_at = models.DateTimeField(null=True, blank=True)
# access tokenهای صادرشده پیش از این لحظه رد می‌شوند (خروج از حساب). توکنِ
# access فقط user_id امضاشده است و به Session وصل نیست؛ بدونِ این، خروج
# فقط refresh را باطل می‌کرد و توکنِ فعلی تا یک ساعت کار می‌کرد.
tokens_valid_after = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return self.name or self.phone or f"user-{self.pk}"
@property
def is_authenticated(self):
# این مدل AUTH_USER_MODEL نیست؛ این property فقط برای سازگاری با
# rest_framework.permissions.IsAuthenticated لازمه.
return True
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
# سالِ تولدِ شمسی. از ۲۰۲۶-۱۰-۰۱ از birth_date پر می‌شود؛ برای کاربرهای
# قدیمی که فقط سال داده‌اند تنها منبع است.
birth_year = models.PositiveSmallIntegerField(null=True, blank=True)
# تاریخِ تولدِ کامل (میلادی ذخیره می‌شود، در API شمسی است؛ accounts/jalali.py).
birth_date = models.DateField(null=True, blank=True)
bio = models.CharField(max_length=200, blank=True)
def __str__(self):
return f"profile-{self.user_id}"
def hash_refresh_token(raw: str) -> str:
"""آنچه در Session.refresh_token ذخیره و با آن جست‌وجو می‌شود؛ خودِ توکن فقط دستِ کلاینت است."""
return hashlib.sha256(raw.encode()).hexdigest()
class Session(models.Model):
"""
یک ورود. توکنِ دسترسی ذخیره نمی‌شود (امضاشده و stateless است، accounts/authentication.py).
`refresh_token` هشِ SHA-256ِ توکنِ تمدید است، نه خودش (hash_refresh_token): هر کس دیتابیس،
یک بکاپ یا پنلِ ادمین را ببیند با آن نمی‌تواند به‌جای کاربر وارد شود. توکن ۶۴ نویسه‌ی تصادفی
است، پس هشِ سریعِ بی‌نمک کافی است.
"""
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sessions')
refresh_token = models.CharField(max_length=200, unique=True)
expires_at = models.DateTimeField()
revoked = models.BooleanField(default=False)
def __str__(self):
return f"session-{self.pk}-user{self.user_id}"
class OTPCode(models.Model):
phone = models.CharField(max_length=15, db_index=True)
code = models.CharField(max_length=6)
expires_at = models.DateTimeField()
is_used = models.BooleanField(default=False)
# تلاش‌های غلط روی همین کد. در دیتابیس شمرده می‌شود، نه در کشِ LocMem که
# برای هر worker جداست؛ بعد از OTP_MAX_FAILED_ATTEMPTS کد باطل می‌شود.
failed_attempts = models.PositiveSmallIntegerField(default=0)
def __str__(self):
return f"otp-{self.phone}"