Coverage for apps / core / models.py: 92%

72 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-02 13:22 +0000

1import os 

2import secrets 

3 

4from django.conf import settings 

5from django.db import models 

6from django.utils import timezone 

7 

8from .encryption import decrypt_value, encrypt_value, is_encrypted 

9 

10# Device code character set: alphanumeric excluding confusables (0, O, 1, I, L) 

11DEVICE_CODE_CHARS = "23456789ABCDEFGHJKMNPQRSTUVWXYZ" # pragma: allowlist secret 

12 

13 

14class AppSettings(models.Model): 

15 """Singleton model for application-wide settings.""" 

16 

17 # Stored encrypted with 'enc:' prefix 

18 _openrouter_api_key = models.CharField(max_length=500, blank=True, db_column="openrouter_api_key") 

19 default_ai_model = models.CharField(max_length=100, default="anthropic/claude-haiku-4.5") 

20 

21 # Per-feature daily AI quota limits (0 = disabled for non-exempt users) 

22 daily_limit_remix = models.PositiveIntegerField(default=1) 

23 daily_limit_remix_suggestions = models.PositiveIntegerField(default=2) 

24 daily_limit_scale = models.PositiveIntegerField(default=2) 

25 daily_limit_tips = models.PositiveIntegerField(default=2) 

26 daily_limit_discover = models.PositiveIntegerField(default=1) 

27 daily_limit_timer = models.PositiveIntegerField(default=3) 

28 

29 class Meta: 

30 verbose_name = "App Settings" 

31 verbose_name_plural = "App Settings" 

32 

33 def save(self, *args, **kwargs): 

34 self.pk = 1 # Enforce singleton 

35 

36 # Encrypt key if not already encrypted 

37 if self._openrouter_api_key and not is_encrypted(self._openrouter_api_key): 

38 self._openrouter_api_key = encrypt_value(self._openrouter_api_key) 

39 

40 super().save(*args, **kwargs) 

41 

42 @classmethod 

43 def get(cls): 

44 """Get or create the singleton instance.""" 

45 obj, _ = cls.objects.get_or_create(pk=1) 

46 return obj 

47 

48 @property 

49 def openrouter_api_key(self) -> str: 

50 """Get the API key. Env var OPENROUTER_API_KEY takes precedence over DB.""" 

51 env_key = os.environ.get("OPENROUTER_API_KEY", "") 

52 if env_key: 

53 return env_key 

54 return decrypt_value(self._openrouter_api_key) 

55 

56 @openrouter_api_key.setter 

57 def openrouter_api_key(self, value: str): 

58 """Set and encrypt the API key.""" 

59 if value and not is_encrypted(value): 

60 self._openrouter_api_key = encrypt_value(value) 

61 else: 

62 self._openrouter_api_key = value 

63 

64 

65class WebAuthnCredential(models.Model): 

66 """Stored passkey credential for a user.""" 

67 

68 user = models.ForeignKey( 

69 settings.AUTH_USER_MODEL, 

70 on_delete=models.CASCADE, 

71 related_name="webauthn_credentials", 

72 ) 

73 credential_id = models.BinaryField(max_length=1024, unique=True) 

74 public_key = models.BinaryField(max_length=1024) 

75 sign_count = models.PositiveIntegerField(default=0) 

76 transports = models.JSONField(null=True, blank=True) 

77 created_at = models.DateTimeField(auto_now_add=True) 

78 last_used_at = models.DateTimeField(null=True, blank=True) 

79 

80 class Meta: 

81 indexes = [ 

82 models.Index(fields=["user"]), 

83 ] 

84 

85 def __str__(self): 

86 return f"WebAuthnCredential(user={self.user_id}, id={self.pk})" 

87 

88 

89def generate_device_code(): 

90 """Generate a 6-character code from the confusable-free character set.""" 

91 return "".join(secrets.choice(DEVICE_CODE_CHARS) for _ in range(6)) 

92 

93 

94class DeviceCode(models.Model): 

95 """Temporary pairing code for the device authorization flow.""" 

96 

97 STATUS_CHOICES = [ 

98 ("pending", "Pending"), 

99 ("authorized", "Authorized"), 

100 ("expired", "Expired"), 

101 ("invalidated", "Invalidated"), 

102 ] 

103 

104 code = models.CharField(max_length=6, unique=True) 

105 status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="pending") 

106 session_key = models.CharField(max_length=40) 

107 authorizing_user = models.ForeignKey( 

108 settings.AUTH_USER_MODEL, 

109 null=True, 

110 blank=True, 

111 on_delete=models.SET_NULL, 

112 related_name="authorized_device_codes", 

113 ) 

114 created_at = models.DateTimeField(auto_now_add=True) 

115 expires_at = models.DateTimeField() 

116 

117 class Meta: 

118 indexes = [ 

119 models.Index(fields=["session_key"]), 

120 models.Index(fields=["expires_at"]), 

121 ] 

122 

123 def __str__(self): 

124 return f"DeviceCode({self.code}, status={self.status})" 

125 

126 def save(self, *args, **kwargs): 

127 if not self.expires_at: 

128 self.expires_at = timezone.now() + timezone.timedelta(seconds=settings.DEVICE_CODE_EXPIRY_SECONDS) 

129 super().save(*args, **kwargs) 

130 

131 @property 

132 def is_expired(self): 

133 return timezone.now() >= self.expires_at 

← Back to Dashboard