# admin.py from django import forms from django.utils import timezone from django.utils.html import escape, mark_safe from django.contrib import admin from django.contrib.admin.widgets import AdminSplitDateTime from .models import PersonalAccessToken, _sha256 # _sha256 dispo dans models.py import secrets _HTML5_FMT = "%Y-%m-%dT%H:%M" # sans secondes ni timezone (exigence de datetime-local) class PersonalAccessTokenForm(forms.ModelForm): # Non obligatoire: on le remplit nous-mêmes si vide token_hash = forms.CharField(required=False, widget=forms.TextInput(attrs={"readonly": "readonly"})) # Aide à la saisie: durée si expires_at non remplit expires_at = forms.DateTimeField( required=False, input_formats=[_HTML5_FMT], # pour le POST widget=forms.DateTimeInput( attrs={"type": "datetime-local"}, format=_HTML5_FMT, # pour l’affichage initial ), ) ttl_days = forms.IntegerField(required=False, min_value=1, initial=30, help_text="Utilisé si 'expires_at' est vide.") class Meta: model = PersonalAccessToken fields = ("user", "name", "collections", "expires_at", "revoked", "token_hash") widgets = { # JSONField par défaut convient; sinon remplacer par un Textarea # "collections": forms.Textarea(attrs={"rows": 2}), } def clean(self): cleaned = super().clean() expires_at = cleaned.get("expires_at") ttl_days = cleaned.get("ttl_days") if not expires_at: # Si pas de date fournie, calcule via ttl_days (ou 30 par défaut) days = ttl_days or 30 cleaned["expires_at"] = timezone.now() + timezone.timedelta(days=days) return cleaned @admin.register(PersonalAccessToken) class PersonalAccessTokenAdmin(admin.ModelAdmin): form = PersonalAccessTokenForm list_display = ("user", "name", "expires_at", "revoked", "created_at") list_filter = ("revoked",) search_fields = ("user__username", "user__email", "name") readonly_fields = ("token_hash",) # visible mais non éditable actions = ["revoke_tokens"] @admin.action(description="Révoquer les tokens sélectionnés") def revoke_tokens(self, request, queryset): updated = queryset.update(revoked=True) self.message_user(request, f"{updated} token(s) révoqué(s).") def save_model(self, request, obj, form, change): """ À la création, si token_hash est vide: - Génère un raw token 'pat_...' - Hash en SHA-256 (même logique que le modèle) - Sauve l'objet - Affiche le raw token une seule fois dans un message admin """ raw_token_to_display = None if not change and not obj.token_hash: # Génère un raw token (même préfixe que la méthode issue) raw = "pat_" + secrets.token_urlsafe(32) obj.token_hash = _sha256(raw) # même algo que le modèle raw_token_to_display = raw # Remise en forme des collections: set trié, comme dans issue() if obj.collections: obj.collections = sorted(set(obj.collections)) # Si l'admin n'a pas rempli expires_at, le form l'a calculé via ttl_days super().save_model(request, obj, form, change) if raw_token_to_display: # Affiche le raw token (copiable) + l'expiration message = f"Token généré pour {obj.user}: {raw_token_to_display}\nExpire le {obj.expires_at:%Y-%m-%d %H:%M}" self.message_user( request, mark_safe("
"+escape(message)+"
") )