import secrets from django.conf import settings from django.contrib.auth.models import AbstractUser from django.db import models from django.utils import timezone class User(AbstractUser): """Project user with optional e621 API credentials and an app role.""" ROLE_USER = "user" ROLE_UPLOADER = "uploader" ROLE_STAFF = "staff" ROLE_CHOICES = [ (ROLE_USER, "User"), (ROLE_UPLOADER, "Uploader"), (ROLE_STAFF, "Staff"), ] role = models.CharField(max_length=20, choices=ROLE_CHOICES, default=ROLE_USER) avatar = models.ForeignKey( "library.MediaItem", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) e621_username = models.CharField(max_length=100, blank=True, default="") # Stored encrypted (see apps/accounts/crypto.py): never the plaintext key. e621_api_key = models.CharField(max_length=400, blank=True, default="") e621_base_url = models.CharField(max_length=200, default="https://e621.net") # Per-user browse preferences (landing page, default filters, grid size). preferences = models.JSONField(default=dict, blank=True) @property def e621_api_key_plain(self): """The decrypted e621 key ("" when it cannot be decrypted).""" from .crypto import decrypt_secret return decrypt_secret(self.e621_api_key) @property def e621_configured(self): return bool(self.e621_username and self.e621_api_key_plain) @property def can_upload(self): return self.is_superuser or self.role in {self.ROLE_UPLOADER, self.ROLE_STAFF} @property def is_app_staff(self): """Staff in this app: superusers, Django staff, or the staff role.""" return bool( self.is_superuser or self.is_staff or self.role == self.ROLE_STAFF ) def hash_bearer_token(value): """SHA-256 of a high-entropy bearer token (no salt needed).""" import hashlib return hashlib.sha256(value.encode()).hexdigest() class GreetingToken(models.Model): """Long-lived token that only authenticates the random-image endpoint. Meant for shell greetings and similar scripts, so it is safe to keep in a config file: it cannot read the library, upload, or touch an account. Only the SHA-256 hash is stored; the plaintext is returned once at creation. """ PREFIX = "j621r_" user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="greeting_tokens", ) key_hash = models.CharField(max_length=64, unique=True) prefix = models.CharField(max_length=16) label = models.CharField(max_length=100, blank=True, default="") created_at = models.DateTimeField(auto_now_add=True) last_used_at = models.DateTimeField(null=True, blank=True) class Meta: ordering = ["-created_at"] def __str__(self): return f"{self.prefix}… ({self.user})" @classmethod def issue(cls, user, label=""): """Create a token and return ``(token, plaintext_key)``.""" key = cls.PREFIX + secrets.token_hex(20) token = cls.objects.create( user=user, key_hash=hash_bearer_token(key), prefix=key[:12], label=label.strip()[:100], ) return token, key @classmethod def resolve(cls, key): if not key.startswith(cls.PREFIX): return None return ( cls.objects.select_related("user") .filter(key_hash=hash_bearer_token(key)) .first() ) def touch(self): GreetingToken.objects.filter(pk=self.pk).update(last_used_at=timezone.now())