Files
dashboard-cpsp/backend/apps/accounts/models.py
T

120 lines
4.1 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)
extra_fields.setdefault("display_name", user_name)
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)
display_name = models.CharField(max_length=100)
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"])