| """ |
| RefreshToken model — tracks token rotation chain. |
| |
| Maps to AGENT.md DBML: `refresh_tokens` table (Lines 202-209). |
| """ |
| from django.conf import settings |
| from django.db import models |
| from django.utils import timezone |
|
|
| from core.models import BaseModel |
|
|
|
|
| class RefreshToken(BaseModel): |
| """ |
| Refresh token with rotation tracking. |
| |
| Each rotation creates a new token linked to its parent via `rotated_from`. |
| Detects token reuse attacks by checking if a revoked token is used again. |
| """ |
|
|
| user = models.ForeignKey( |
| settings.AUTH_USER_MODEL, |
| on_delete=models.CASCADE, |
| related_name="refresh_tokens", |
| ) |
| rotated_from = models.UUIDField( |
| null=True, |
| blank=True, |
| help_text="Parent token ID this was rotated from.", |
| ) |
| expires_at = models.DateTimeField() |
| revoked_at = models.DateTimeField(null=True, blank=True) |
|
|
| class Meta: |
| db_table = "refresh_tokens" |
| ordering = ["-created_at"] |
| indexes = [ |
| models.Index(fields=["user", "-created_at"]), |
| ] |
|
|
| def __str__(self) -> str: |
| return f"RefreshToken {self.pk} for {self.user_id}" |
|
|
| @property |
| def is_active(self) -> bool: |
| return self.revoked_at is None and self.expires_at > timezone.now() |
|
|
| def revoke(self) -> None: |
| self.revoked_at = timezone.now() |
| self.save(update_fields=["revoked_at"]) |
|
|