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>
This commit is contained in:
Daniel Bedeleanu
2026-04-11 13:42:09 +03:00
parent e9ada00497
commit e6ca33f2f0
6 changed files with 181 additions and 55 deletions

View File

@@ -1,6 +1,8 @@
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from slowapi import Limiter
from slowapi.util import get_remote_address
from . import models
from .database import engine
from .routers import items, operations, users, categories
@@ -13,17 +15,26 @@ log.info("Database tables verified.")
app = FastAPI(title="TFM aInventory API", version="1.1.0")
log.info("TFM aInventory API process started.")
# [H-02] Rate limiting pe API
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler = limiter.add_exception_handler
# [SECURITY FIX M-01] CORS: allow_origins=["*"] + allow_credentials=True este invalid per spec.
# Originile permise se configurează via variabila de mediu ALLOWED_ORIGINS (comma-separated).
# Fallback sigur: doar localhost pentru development.
_raw_origins = os.environ.get("ALLOWED_ORIGINS", "http://localhost:3000,http://localhost:3002")
_raw_origins = os.environ.get(
"ALLOWED_ORIGINS",
"http://localhost:3000,http://localhost:3002"
)
ALLOWED_ORIGINS = [o.strip() for o in _raw_origins.split(",") if o.strip()]
log.info(f"CORS allowed origins: {ALLOWED_ORIGINS}")
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["*"],
)

View File

@@ -1,10 +1,15 @@
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
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"]
@@ -55,12 +60,13 @@ def read_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 pe endpoint."""
"""[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ă