Files
tfm_ainventory/backend/routers/items.py
Daniel Bedeleanu e6ca33f2f0 feat: frontend JWT handling, rate limiting [H-02], CORS config
Frontend:
- Creiez frontend/lib/auth.ts cu saveToken, getToken, getAuthHeader, clearAuth
- Modific api.ts: axiosInstance cu interceptor Bearer token + 401 → /login redirect
- Modific login page: salveaza JWT token din response

Backend:
- [H-02] Integrez slowapi rate limiting: 10 req/minute pe /items/extract-label
- [M-01] CORS: ALLOWED_ORIGINS din env (dev fallback: localhost:3000, localhost:3002)
- [C-01] JWT_SECRET_KEY din env (dev fallback: ephemeral key)

docker-compose.yml:
- Adaug ALLOWED_ORIGINS env var (dev: localhost)
- Adaug JWT_SECRET_KEY env var cu fallback warning

Status:
-  JWT backend: complet
-  JWT frontend: token save + attach + 401 handling
-  Rate limiting: 10/min pe extract-label
-  CORS: configurable via env

Gata pentru dev + testing local.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 13:42:09 +03:00

153 lines
5.1 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Request
from sqlalchemy.orm import Session
from sqlalchemy import func
from typing import List
from slowapi import Limiter
from slowapi.util import get_remote_address
from .. import models, schemas, auth
from ..database import get_db
# [H-02] Rate limiter pentru extract-label endpoint
limiter = Limiter(key_func=get_remote_address)
router = APIRouter(
prefix="/items",
tags=["Items"]
)
@router.get("/stats")
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,
"items_distribution": {cat: count for cat, count in items_per_category if cat}
}
@router.get("/", response_model=List[schemas.Item])
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),
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")
return item
_ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png", "image/webp", "image/gif"}
_MAX_IMAGE_SIZE = 10 * 1024 * 1024 # 10 MB
@limiter.limit("10/minute")
@router.post("/extract-label")
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: 10 req/min per IP."""
from ..ai_vision import extract_label_info
# [SECURITY FIX H-03] Validare tip MIME și dimensiune maximă
if file.content_type not in _ALLOWED_IMAGE_TYPES:
raise HTTPException(
status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
detail=f"Tip fișier nepermis: {file.content_type}. Acceptat: {', '.join(_ALLOWED_IMAGE_TYPES)}"
)
contents = await file.read()
if len(contents) > _MAX_IMAGE_SIZE:
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail="Fișierul depășește limita de 10MB."
)
result = extract_label_info(contents)
return result
@router.post("/", response_model=schemas.Item, status_code=status.HTTP_201_CREATED)
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 — [M-02] user_id din token, nu din body
audit = models.AuditLog(
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),
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),
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"}