from __future__ import annotations import secrets from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ def _generate_token() -> str: return secrets.token_urlsafe(32) class TokenKind(models.TextChoices): READ = "read", _("Read (non-public visibility)") INGEST = "ingest", _("Ingest (push)") class ApiToken(models.Model): """Static API token for machine clients. Not tied to a user, no scopes. Read tokens extend visibility to non-public records; ingest tokens may also push data (ground-station frames in tdas, station logs in ops). A service that has nothing to ingest simply never issues the second kind. """ name = models.CharField( max_length=64, unique=True, help_text=_("Human-readable label, e.g. 'TMTC pipeline' or 'ground station 1'."), ) token = models.CharField( max_length=128, unique=True, default=_generate_token, editable=False, db_index=True, help_text=_("Bearer credential. Treat as a secret."), ) kind = models.CharField( max_length=16, choices=TokenKind.choices, default=TokenKind.READ, help_text=_("Read tokens extend visibility; ingest tokens may also push data."), ) is_active = models.BooleanField(default=True, db_index=True) created_at = models.DateTimeField(auto_now_add=True) last_used_at = models.DateTimeField(null=True, blank=True) notes = models.TextField(blank=True) class Meta: ordering = ["-created_at"] verbose_name = _("API token") verbose_name_plural = _("API tokens") def __str__(self) -> str: return self.name def mark_used(self) -> None: # Written on every authenticated request, so it must not touch the rest # of the row: a full save() here would clobber a concurrent admin edit. self.last_used_at = timezone.now() type(self).objects.filter(pk=self.pk).update(last_used_at=self.last_used_at)