FossilRepo

fossilrepo / accounts / models.py
Blame History Raw 89 lines
1
"""User profile and personal access token models.
2
3
UserProfile extends Django's built-in User with optional profile fields.
4
PersonalAccessToken provides user-scoped tokens for API/CLI authentication,
5
separate from project-scoped APITokens.
6
"""
7
8
import hashlib
9
import re
10
import secrets
11
12
from django.contrib.auth.models import User
13
from django.db import models
14
from django.utils import timezone
15
16
17
class UserProfile(models.Model):
18
"""Extended profile information for users."""
19
20
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")
21
handle = models.CharField(
22
max_length=50,
23
blank=True,
24
null=True,
25
default=None,
26
unique=True,
27
help_text="@handle for mentions (alphanumeric and hyphens only)",
28
)
29
bio = models.TextField(blank=True, default="", max_length=500)
30
location = models.CharField(max_length=100, blank=True, default="")
31
website = models.URLField(blank=True, default="")
32
33
def __str__(self):
34
return f"@{self.handle or self.user.username}"
35
36
@staticmethod
37
def sanitize_handle(raw: str) -> str:
38
"""Slugify a handle: lowercase, alphanumeric + hyphens, strip leading/trailing hyphens."""
39
cleaned = re.sub(r"[^a-z0-9-]", "", raw.lower().strip())
40
return cleaned.strip("-")
41
42
43
class PersonalAccessToken(models.Model):
44
"""User-scoped personal access token for API/CLI authentication.
45
46
Tokens are stored as SHA-256 hashes -- the raw value is shown once on
47
creation and never stored in plaintext.
48
"""
49
50
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="personal_tokens")
51
name = models.CharField(max_length=200)
52
token_hash = models.CharField(max_length=64, unique=True)
53
token_prefix = models.CharField(max_length=12)
54
scopes = models.CharField(max_length=500, default="read", help_text="Comma-separated: read, write, admin")
55
expires_at = models.DateTimeField(null=True, blank=True)
56
last_used_at = models.DateTimeField(null=True, blank=True)
57
created_at = models.DateTimeField(auto_now_add=True)
58
revoked_at = models.DateTimeField(null=True, blank=True)
59
60
class Meta:
61
ordering = ["-created_at"]
62
63
@staticmethod
64
def generate():
65
"""Generate a new token. Returns (raw_token, token_hash, prefix)."""
66
raw = f"frp_{secrets.token_urlsafe(32)}"
67
hash_val = hashlib.sha256(raw.encode()).hexdigest()
68
prefix = raw[:12]
69
return raw, hash_val, prefix
70
71
@staticmethod
72
def hash_token(raw_token):
73
return hashlib.sha256(raw_token.encode()).hexdigest()
74
75
@property
76
def is_expired(self):
77
return bool(self.expires_at and self.expires_at < timezone.now())
78
79
@property
80
def is_revoked(self):
81
return self.revoked_at is not None
82
83
@property
84
def is_active(self):
85
return not self.is_expired and not self.is_revoked
86
87
def __str__(self):
88
return f"{self.name} ({self.token_prefix}...)"
89

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button