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

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.""" 

8 

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 

15 

16 

17class ApiKey(SeedboxSyncModel): 

18 """ 

19 Data Access Object (DAO) representing an API Key. 

20 

21 Stores API key metadata and hashed secret values associated with a user 

22 account for programmatic access. 

23 

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 """ 

32 

33 KEY_PREFIX = "sbx_" 

34 

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") 

41 

42 @classmethod 

43 def generate(cls, user: User, name: str) -> tuple[Self, str]: 

44 """ 

45 Generate a new API key for a user. 

46 

47 Computes a cryptographically secure random token, hashes it for storage, 

48 and returns both the database model instance and the raw token. 

49 

50 Args: 

51 user (User): User model instance owning the new key. 

52 name (str): Descriptive label for the key. 

53 

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() 

61 

62 api_key = cls.create( 

63 user=user, 

64 name=name, 

65 key_hash=key_hash, 

66 ) 

67 

68 return api_key, raw_key 

69 

70 @classmethod 

71 def authenticate(cls, raw_key: str) -> User | None: 

72 """ 

73 Authenticate an incoming API key string. 

74 

75 Hashes the incoming raw key and verifies if a matching active key exists 

76 in the database. Updates `last_used` on success. 

77 

78 Args: 

79 raw_key (str): Plain-text API key provided in the HTTP header/request. 

80 

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 

87 

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() 

90 

91 if api_key is None: 

92 return None 

93 

94 # Update last_used timestamp 

95 api_key.last_used = datetime.datetime.now() 

96 api_key.save() 

97 

98 return cast(User, api_key.user)