feat: implementare JWT Bearer authentication pe toți routers [C-01]

Implementare completă a autentificării Bearer token:
- Creiez backend/auth.py: funcții JWT (create_access_token, get_current_user, get_current_admin)
- Modific /users/login: returnează TokenResponse cu JWT token și expirare 8h
- Adaug Depends(get_current_user) pe toate endpoint-urile API
- [M-02] user_id extras din JWT token, nu din request body
- [L-01] Token cu exp claim pentru sesiuni frontend

Acces endpoints-uri:
- GET/POST /users/: authenticated users
- POST /users/: admin only
- PUT /users/{id}, DELETE /users/{id}, /ldap-config, /test-ldap: admin only
- Toți routers (items, operations, categories): authenticated users minimum

Modificări dependențe:
- Adaug: python-jose[cryptography]>=3.3.0, slowapi>=0.1.9 (pentru H-02 rate limiting)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Daniel Bedeleanu
2026-04-11 13:38:24 +03:00
parent 247ea45408
commit 9b6adad618
7 changed files with 338 additions and 97 deletions

100
backend/auth.py Normal file
View File

@@ -0,0 +1,100 @@
"""
[C-01] JWT Authentication Module
Implementare Bearer token authentication pentru API endpoints.
"""
import os
from datetime import datetime, timedelta, timezone
from typing import Optional
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthCredentials
from jose import JWTError, jwt
from pydantic import BaseModel
# Configuration
SECRET_KEY = os.environ.get("JWT_SECRET_KEY")
if not SECRET_KEY:
# Genereaza o cheie de fallback pentru dev (NU PENTRU PRODUCȚIE)
import secrets
SECRET_KEY = secrets.token_urlsafe(32)
import sys
print(f"[WARNING] JWT_SECRET_KEY not set. Generated ephemeral key: {SECRET_KEY[:20]}...", file=sys.stderr)
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 480 # 8 hours
security = HTTPBearer()
class TokenData(BaseModel):
sub: int # user_id
username: str
role: str
exp: datetime
class TokenResponse(BaseModel):
access_token: str
token_type: str
user_id: int
username: str
role: str
def create_access_token(user_id: int, username: str, role: str, expires_delta: Optional[timedelta] = None):
"""Create JWT token with expiration."""
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode = {
"sub": user_id,
"username": username,
"role": role,
"exp": expire,
"iat": datetime.now(timezone.utc)
}
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
async def get_current_user(credentials: HTTPAuthCredentials = Depends(security)):
"""
Dependency que valideaza JWT token din Authorization header.
Returneaza TokenData cu user_id, username, role.
"""
token = credentials.credentials
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id: int = payload.get("sub")
username: str = payload.get("username")
role: str = payload.get("role")
if user_id is None or username is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token claims"
)
token_data = TokenData(
sub=user_id,
username=username,
role=role,
exp=datetime.fromtimestamp(payload.get("exp"), tz=timezone.utc)
)
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return token_data
async def get_current_admin(current_user: TokenData = Depends(get_current_user)):
"""Dependency que verifica daca user-ul are rol 'admin'."""
if current_user.role != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin role required"
)
return current_user