Coverage for seedboxsync/core/database/models/apikey.py: 100%
31 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-26 17:14 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-26 17:14 +0000
1#
2# Copyright (C) 2015-2026 Guillaume Kulakowski <guillaume@kulakowski.fr>
3#
4# For the full copyright and license information, please view the LICENSE
5# file that was distributed with this source code.
6#
7"""Peewee DAO model for ApiKey."""
9import datetime
10import hashlib
11import secrets
12from typing import Self, cast
13from peewee import AutoField, CharField, DateTimeField, ForeignKeyField
14from seedboxsync.core.database.models import SeedboxSyncModel, User
17class ApiKey(SeedboxSyncModel):
18 """
19 Data Access Object (DAO) representing an API Key.
21 Stores API key metadata and hashed secret values associated with a user
22 account for programmatic access.
24 Attributes:
25 id (int): Auto-incremented primary key.
26 user (User): Foreign key reference to the associated user account.
27 name (str): Human-readable label describing the purpose of the key.
28 key_hash (str): SHA-256 hash of the generated raw API key.
29 created (datetime): Timestamp when the API key was created.
30 last_used (datetime | None): Timestamp when the API key was last used.
31 """
33 KEY_PREFIX = "sbx_"
35 id = AutoField(help_text="Unique identifier of the API key")
36 user = ForeignKeyField(User, backref="api_keys", on_delete="CASCADE", help_text="Owner of the API key")
37 name = CharField(max_length=64, help_text="Description or label for the API key")
38 key_hash = CharField(unique=True, help_text="SHA-256 hash of the plain-text API key")
39 created = DateTimeField(default=datetime.datetime.now, help_text="Timestamp when the key was created")
40 last_used = DateTimeField(null=True, help_text="Timestamp when the key was last authenticated")
42 @classmethod
43 def generate(cls, user: User, name: str) -> tuple[Self, str]:
44 """
45 Generate a new API key for a user.
47 Computes a cryptographically secure random token, hashes it for storage,
48 and returns both the database model instance and the raw token.
50 Args:
51 user (User): User model instance owning the new key.
52 name (str): Descriptive label for the key.
54 Returns:
55 tuple[Self, str]: A tuple containing (ApiKey instance, raw_api_key_str).
56 The raw key string must be displayed to the user immediately,
57 as it cannot be recovered later.
58 """
59 raw_key = f"{cls.KEY_PREFIX}{secrets.token_urlsafe(32)}"
60 key_hash = hashlib.sha256(raw_key.encode("utf-8")).hexdigest()
62 api_key = cls.create(
63 user=user,
64 name=name,
65 key_hash=key_hash,
66 )
68 return api_key, raw_key
70 @classmethod
71 def authenticate(cls, raw_key: str) -> User | None:
72 """
73 Authenticate an incoming API key string.
75 Hashes the incoming raw key and verifies if a matching active key exists
76 in the database. Updates `last_used` on success.
78 Args:
79 raw_key (str): Plain-text API key provided in the HTTP header/request.
81 Returns:
82 User | None: The matching User instance,
83 or None if authentication fails.
84 """
85 if not raw_key or not raw_key.startswith(cls.KEY_PREFIX):
86 return None
88 key_hash = hashlib.sha256(raw_key.encode("utf-8")).hexdigest()
89 api_key = cls.select(cls, User).join(User).where(cls.key_hash == key_hash).first()
91 if api_key is None:
92 return None
94 # Update last_used timestamp
95 api_key.last_used = datetime.datetime.now()
96 api_key.save()
98 return cast(User, api_key.user)