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

View File

@@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
from sqlalchemy.orm import Session
from sqlalchemy import func
from typing import List
from .. import models, schemas
from .. import models, schemas, auth
from ..database import get_db
router = APIRouter(
@@ -11,14 +11,18 @@ router = APIRouter(
)
@router.get("/stats")
def read_item_stats(db: Session = Depends(get_db)):
def read_item_stats(
db: Session = Depends(get_db),
current_user: auth.TokenData = Depends(auth.get_current_user)
):
"""[C-01] Statistici iteme — doar utilizatori autentificati."""
total_categories = db.query(models.Category).count()
total_items = db.query(models.Item).count()
# Count items per category string
items_per_category = db.query(models.Item.category, func.count(models.Item.id))\
.group_by(models.Item.category).all()
return {
"total_categories": total_categories,
"total_items": total_items,
@@ -26,12 +30,23 @@ def read_item_stats(db: Session = Depends(get_db)):
}
@router.get("/", response_model=List[schemas.Item])
def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
def read_items(
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
current_user: auth.TokenData = Depends(auth.get_current_user)
):
"""[C-01] Lista iteme — doar utilizatori autentificati."""
items = db.query(models.Item).offset(skip).limit(limit).all()
return items
@router.get("/{item_id}", response_model=schemas.Item)
def read_item(item_id: int, db: Session = Depends(get_db)):
def read_item(
item_id: int,
db: Session = Depends(get_db),
current_user: auth.TokenData = Depends(auth.get_current_user)
):
"""[C-01] Obține item — doar utilizatori autentificati."""
item = db.query(models.Item).filter(models.Item.id == item_id).first()
if item is None:
raise HTTPException(status_code=404, detail="Item not found")
@@ -41,7 +56,11 @@ _ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png", "image/webp", "image/gif"}
_MAX_IMAGE_SIZE = 10 * 1024 * 1024 # 10 MB
@router.post("/extract-label")
async def extract_label(file: UploadFile = File(...)):
async def extract_label(
file: UploadFile = File(...),
current_user: auth.TokenData = Depends(auth.get_current_user)
):
"""[C-01] Extragere etichetă din imagine — doar utilizatori autentificati. [H-02] Rate limit pe endpoint."""
from ..ai_vision import extract_label_info
# [SECURITY FIX H-03] Validare tip MIME și dimensiune maximă
@@ -63,49 +82,65 @@ async def extract_label(file: UploadFile = File(...)):
return result
@router.post("/", response_model=schemas.Item, status_code=status.HTTP_201_CREATED)
def create_item(item: schemas.ItemCreate, user_id: int, db: Session = Depends(get_db)):
def create_item(
item: schemas.ItemCreate,
db: Session = Depends(get_db),
current_user: auth.TokenData = Depends(auth.get_current_user)
):
"""[C-01] Creare item — doar utilizatori autentificati. [M-02] user_id din token."""
# Check if barcode exists
db_item = db.query(models.Item).filter(models.Item.barcode == item.barcode).first()
if db_item:
raise HTTPException(status_code=400, detail="Barcode already registered")
db_item = models.Item(**item.model_dump())
db.add(db_item)
db.commit()
db.refresh(db_item)
# Audit log the creation
# Audit log the creation — [M-02] user_id din token, nu din body
audit = models.AuditLog(
user_id=user_id,
user_id=current_user.sub,
action="CREATE_ITEM",
target_item_id=db_item.id,
quantity_change=item.quantity
)
db.add(audit)
db.commit()
return db_item
@router.put("/{item_id}", response_model=schemas.Item)
def update_item(item_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db)):
def update_item(
item_id: int,
item: schemas.ItemCreate,
db: Session = Depends(get_db),
current_user: auth.TokenData = Depends(auth.get_current_user)
):
"""[C-01] Actualizare item — doar utilizatori autentificati."""
db_item = db.query(models.Item).filter(models.Item.id == item_id).first()
if not db_item:
raise HTTPException(status_code=404, detail="Item not found")
update_data = item.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(db_item, key, value)
db.commit()
db.refresh(db_item)
return db_item
@router.delete("/{item_id}")
def delete_item(item_id: int, db: Session = Depends(get_db)):
def delete_item(
item_id: int,
db: Session = Depends(get_db),
current_user: auth.TokenData = Depends(auth.get_current_user)
):
"""[C-01] Ștergere item — doar utilizatori autentificati."""
db_item = db.query(models.Item).filter(models.Item.id == item_id).first()
if not db_item:
raise HTTPException(status_code=404, detail="Item not found")
db.delete(db_item)
db.commit()
return {"message": "Item deleted successfully"}