loko/streetup/api/models.py
2026-07-22 14:48:40 +02:00

61 lines
2 KiB
Python

from __future__ import annotations
import hashlib
import secrets
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import models
from django.utils import timezone
User = get_user_model()
def _sha256(s: str) -> str:
return hashlib.sha256(s.encode("utf-8")).hexdigest()
class PersonalAccessToken(models.Model):
"""
Token opaque (PAT) stocké haché pour liens publics ou intégrations.
Révocable instantanément + expiration.
"""
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="personal_access_tokens")
name = models.CharField(max_length=120, blank=True)
token_hash = models.CharField(max_length=64, unique=True, db_index=True)
collections = models.JSONField(default=list) # ex: ["technical-galleries", "assets"]
expires_at = models.DateTimeField()
revoked = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ("-created_at",)
def __str__(self) -> str:
return f"PAT({self.user}, {self.name or 'unnamed'})"
@property
def is_expired(self) -> bool:
return timezone.now() >= self.expires_at
@property
def is_active(self) -> bool:
return (not self.revoked) and (not self.is_expired)
@classmethod
def issue(cls, *, user, collections: list[str], ttl_hours: int = 720, name: str = "share link") -> tuple[str, "PersonalAccessToken"]:
raw = "pat_" + secrets.token_urlsafe(32)
obj = cls.objects.create(
user=user,
name=name,
token_hash=_sha256(raw),
collections=sorted(set(collections)),
expires_at=timezone.now() + timezone.timedelta(hours=int(ttl_hours)),
)
return raw, obj
@classmethod
def lookup(cls, raw: str) -> "PersonalAccessToken | None":
try:
obj = cls.objects.get(token_hash=_sha256(raw))
return obj if obj.is_active else None
except cls.DoesNotExist:
return None