61 lines
2 KiB
Python
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
|