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}" class LoginEvent(models.Model): """ سابقه‌ی ماندگارِ ورود و خروج، برای پرسش‌هایی مثلِ «این کاربر کی آمد، کی رفت». چرا جدول و نه فقط لاگ: لاگِ stdout با ری‌استارت یا چرخشِ journald از بین می‌رود؛ این ردیف‌ها در دیتابیس و بکاپ می‌مانند. `user_id` ارجاعِ سبک است (نه FK، مثلِ بقیه‌ی پروژه)، و ردیف با حذفِ حساب نمی‌ماند مگر در KEPT_ON_ACCOUNT_DELETE. شماره‌ی کامل برای ادمین‌ها ذخیره می‌شود (تصمیمِ مالک، ۲۰۲۶-۱۰-۰۸)؛ در سیاستِ حریمِ خصوصی باید بیاید. """ LOGIN_OK = 'login_ok' LOGIN_FAILED = 'login_failed' # کدِ غلط یا منقضی LOGIN_BLOCKED = 'login_blocked' # سقفِ تلاشِ شماره (429) LOGOUT = 'logout' ACCOUNT_DELETED = 'account_deleted' EVENT_CHOICES = [ (LOGIN_OK, 'ورود موفق'), (LOGIN_FAILED, 'ورود ناموفق'), (LOGIN_BLOCKED, 'ورود مسدود'), (LOGOUT, 'خروج'), (ACCOUNT_DELETED, 'حذف حساب'), ] event = models.CharField(max_length=20, choices=EVENT_CHOICES, db_index=True) user_id = models.IntegerField(null=True, blank=True, db_index=True) phone = models.CharField(max_length=15, blank=True, db_index=True) ip = models.GenericIPAddressField(null=True, blank=True) user_agent = models.CharField(max_length=300, blank=True) created_at = models.DateTimeField(auto_now_add=True, db_index=True) class Meta: ordering = ['-created_at'] def __str__(self): return f"{self.event} user={self.user_id} {self.created_at:%Y-%m-%d %H:%M}"