118 lines
3.9 KiB
Python
118 lines
3.9 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import secrets
|
|
|
|
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
|
|
from django.db import models
|
|
from django.utils import timezone
|
|
|
|
|
|
class UserManager(BaseUserManager):
|
|
def create_user(self, user_name: str, password: str | None = None, **extra_fields):
|
|
if not user_name:
|
|
raise ValueError("user_name is required")
|
|
extra_fields.setdefault("status", User.STATUS_ACTIVE)
|
|
user = self.model(user_name=user_name, **extra_fields)
|
|
user.set_password(password)
|
|
user.save(using=self._db)
|
|
return user
|
|
|
|
def create_superuser(self, user_name: str, password: str | None = None, **extra_fields):
|
|
extra_fields["status"] = User.STATUS_SUPERADMIN
|
|
extra_fields.setdefault("is_superuser", True)
|
|
extra_fields.setdefault("is_active", True)
|
|
return self.create_user(user_name, password, **extra_fields)
|
|
|
|
|
|
class User(AbstractBaseUser, PermissionsMixin):
|
|
"""ERD User Access — username field is user_name."""
|
|
|
|
STATUS_ACTIVE = "active"
|
|
STATUS_INACTIVE = "inactive"
|
|
STATUS_SUPERADMIN = "superadmin"
|
|
STATUS_CHOICES = [
|
|
(STATUS_ACTIVE, "Active"),
|
|
(STATUS_INACTIVE, "Inactive"),
|
|
(STATUS_SUPERADMIN, "Super Admin"),
|
|
]
|
|
|
|
user_id = models.BigAutoField(primary_key=True)
|
|
user_name = models.CharField(max_length=30, unique=True)
|
|
status = models.CharField(max_length=30, choices=STATUS_CHOICES, default=STATUS_ACTIVE)
|
|
is_staff = models.BooleanField(default=False)
|
|
is_active = models.BooleanField(default=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
objects = UserManager()
|
|
|
|
USERNAME_FIELD = "user_name"
|
|
REQUIRED_FIELDS: list[str] = []
|
|
|
|
class Meta:
|
|
db_table = "user_access"
|
|
ordering = ["user_name"]
|
|
|
|
def __str__(self) -> str:
|
|
return self.user_name
|
|
|
|
def save(self, *args, **kwargs):
|
|
if self.status == self.STATUS_INACTIVE:
|
|
self.is_active = False
|
|
self.is_staff = False
|
|
self.is_superuser = False
|
|
elif self.status == self.STATUS_SUPERADMIN:
|
|
self.is_active = True
|
|
self.is_staff = False
|
|
self.is_superuser = True
|
|
else:
|
|
self.is_active = True
|
|
self.is_superuser = False
|
|
# is_staff is left untouched for active users, so an operator can be
|
|
# explicitly flagged as staff without being a superadmin.
|
|
super().save(*args, **kwargs)
|
|
|
|
|
|
class ApiKey(models.Model):
|
|
"""Hashed API keys for X-API-Key authentication."""
|
|
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="api_keys")
|
|
name = models.CharField(max_length=100)
|
|
prefix = models.CharField(max_length=12, db_index=True)
|
|
key_hash = models.CharField(max_length=64)
|
|
is_active = models.BooleanField(default=True)
|
|
last_used_at = models.DateTimeField(null=True, blank=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = "api_keys"
|
|
ordering = ["-created_at"]
|
|
|
|
def __str__(self) -> str:
|
|
return f"{self.name} ({self.prefix}…)"
|
|
|
|
@staticmethod
|
|
def hash_key(raw_key: str) -> str:
|
|
return hashlib.sha256(raw_key.encode("utf-8")).hexdigest()
|
|
|
|
@classmethod
|
|
def generate(cls, user: User, name: str) -> tuple[ApiKey, str]:
|
|
raw = secrets.token_urlsafe(32)
|
|
prefix = raw[:8]
|
|
instance = cls.objects.create(
|
|
user=user,
|
|
name=name,
|
|
prefix=prefix,
|
|
key_hash=cls.hash_key(raw),
|
|
)
|
|
return instance, raw
|
|
|
|
def verify(self, raw_key: str) -> bool:
|
|
return secrets.compare_digest(self.key_hash, self.hash_key(raw_key))
|
|
|
|
def touch(self) -> None:
|
|
self.last_used_at = timezone.now()
|
|
self.save(update_fields=["last_used_at", "updated_at"])
|