Coverage for app / core / security.py: 100%

31 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-04 06:09 -0500

1import secrets 

2import hashlib 

3from datetime import datetime, timedelta, timezone 

4from typing import Optional 

5 

6from jose import jwt 

7from passlib.context import CryptContext 

8 

9from app.core.config import settings 

10 

11pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 

12 

13 

14def create_api_key() -> str: 

15 """Generate a new API key.""" 

16 return secrets.token_urlsafe(32) 

17 

18 

19def hash_api_key(api_key: str) -> str: 

20 """Hash an API key for storage.""" 

21 salted = f"{settings.api_key_salt}{api_key}" 

22 return hashlib.sha256(salted.encode()).hexdigest() 

23 

24 

25def verify_api_key(plain_key: str, hashed_key: str) -> bool: 

26 """Verify an API key against its hash.""" 

27 return hash_api_key(plain_key) == hashed_key 

28 

29 

30def create_magic_link_token() -> str: 

31 """Generate a magic link token.""" 

32 return secrets.token_urlsafe(32) 

33 

34 

35def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: 

36 """Create a JWT access token.""" 

37 to_encode = data.copy() 

38 if expires_delta: 

39 expire = datetime.now(timezone.utc) + expires_delta 

40 else: 

41 expire = datetime.now(timezone.utc) + timedelta(minutes=settings.access_token_expire_minutes) 

42 to_encode.update({"exp": expire}) 

43 encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm) 

44 return encoded_jwt 

45 

46 

47def decode_access_token(token: str) -> Optional[dict]: 

48 """Decode and verify a JWT access token.""" 

49 try: 

50 payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm]) 

51 return payload 

52 except jwt.JWTError: 

53 return None