yksa-django-kit/yksa_kit/models.py
2026-08-17 22:50:10 +08:00

64 lines
2 KiB
Python

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)