From 9b6adad618ad8d8f540ce720da10adbc2f246526 Mon Sep 17 00:00:00 2001 From: Daniel Bedeleanu Date: Sat, 11 Apr 2026 13:38:24 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20implementare=20JWT=20Bearer=20authentic?= =?UTF-8?q?ation=20pe=20to=C8=9Bi=20routers=20[C-01]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- backend/auth.py | 100 +++++++++++++++++++++++++++++ backend/requirements.txt | 2 + backend/routers/categories.py | 40 +++++++++--- backend/routers/items.py | 71 +++++++++++++++------ backend/routers/operations.py | 114 +++++++++++++++++++++------------- backend/routers/users.py | 101 ++++++++++++++++++++++-------- backend/schemas.py | 7 +++ 7 files changed, 338 insertions(+), 97 deletions(-) create mode 100644 backend/auth.py diff --git a/backend/auth.py b/backend/auth.py new file mode 100644 index 00000000..8130c627 --- /dev/null +++ b/backend/auth.py @@ -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 diff --git a/backend/requirements.txt b/backend/requirements.txt index 1b28d712..0bf1fd49 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -10,3 +10,5 @@ Pillow>=10.0.0 python-multipart>=0.0.9 ldap3>=2.9.1 passlib[bcrypt]>=1.7.4 +python-jose[cryptography]>=3.3.0 +slowapi>=0.1.9 diff --git a/backend/routers/categories.py b/backend/routers/categories.py index eb069366..526122eb 100644 --- a/backend/routers/categories.py +++ b/backend/routers/categories.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from typing import List -from .. import models, schemas, database +from .. import models, schemas, auth, database router = APIRouter(prefix="/categories", tags=["categories"]) @@ -13,7 +13,11 @@ def get_db(): db.close() @router.get("/", response_model=List[schemas.Category]) -def get_categories(db: Session = Depends(get_db)): +def get_categories( + db: Session = Depends(get_db), + current_user: auth.TokenData = Depends(auth.get_current_user) +): + """[C-01] Lista categorii — doar utilizatori autentificati.""" categories = db.query(models.Category).all() # Auto-seed if empty with defaults mentioned by user if not categories: @@ -31,11 +35,16 @@ def get_categories(db: Session = Depends(get_db)): return categories @router.post("/", response_model=schemas.Category) -def create_category(category: schemas.CategoryCreate, db: Session = Depends(get_db)): +def create_category( + category: schemas.CategoryCreate, + db: Session = Depends(get_db), + current_user: auth.TokenData = Depends(auth.get_current_user) +): + """[C-01] Creare categorie — doar utilizatori autentificati.""" existing = db.query(models.Category).filter(models.Category.name == category.name).first() if existing: raise HTTPException(status_code=400, detail="Category already exists") - + new_cat = models.Category(**category.model_dump()) db.add(new_cat) db.commit() @@ -43,30 +52,41 @@ def create_category(category: schemas.CategoryCreate, db: Session = Depends(get_ return new_cat @router.put("/{cat_id}", response_model=schemas.Category) -def update_category(cat_id: int, category: schemas.CategoryCreate, db: Session = Depends(get_db)): +def update_category( + cat_id: int, + category: schemas.CategoryCreate, + db: Session = Depends(get_db), + current_user: auth.TokenData = Depends(auth.get_current_user) +): + """[C-01] Actualizare categorie — doar utilizatori autentificati.""" db_cat = db.query(models.Category).filter(models.Category.id == cat_id).first() if not db_cat: raise HTTPException(status_code=404, detail="Category not found") - + update_data = category.model_dump(exclude_unset=True) for key, value in update_data.items(): setattr(db_cat, key, value) - + db.commit() db.refresh(db_cat) return db_cat @router.delete("/{cat_id}") -def delete_category(cat_id: int, db: Session = Depends(get_db)): +def delete_category( + cat_id: int, + db: Session = Depends(get_db), + current_user: auth.TokenData = Depends(auth.get_current_user) +): + """[C-01] Ștergere categorie — doar utilizatori autentificati.""" cat = db.query(models.Category).filter(models.Category.id == cat_id).first() if not cat: raise HTTPException(status_code=404, detail="Category not found") - + # Check if items are linked linkedItems = db.query(models.Item).filter(models.Item.category_id == cat_id).count() if linkedItems > 0: raise HTTPException(status_code=400, detail="Cannot delete category with linked items") - + db.delete(cat) db.commit() return {"message": "Category deleted"} diff --git a/backend/routers/items.py b/backend/routers/items.py index 3638b1b6..8f1a73d2 100644 --- a/backend/routers/items.py +++ b/backend/routers/items.py @@ -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"} diff --git a/backend/routers/operations.py b/backend/routers/operations.py index 96995846..ec9028ec 100644 --- a/backend/routers/operations.py +++ b/backend/routers/operations.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, Depends, HTTPException, status from typing import List from sqlalchemy.orm import Session -from .. import models, schemas +from .. import models, schemas, auth from ..database import get_db router = APIRouter( @@ -10,125 +10,150 @@ router = APIRouter( ) @router.post("/check-in", response_model=schemas.Item) -def check_in_item(op: schemas.OperationCreate, db: Session = Depends(get_db)): +def check_in_item( + op: schemas.OperationCreate, + db: Session = Depends(get_db), + current_user: auth.TokenData = Depends(auth.get_current_user) +): + """[C-01] Check-in item — doar utilizatori autentificati. [M-02] user_id din token.""" if op.quantity <= 0: - raise HTTPException(status_code=400, detail="Quantity must be greater than zero") - + raise HTTPException(status_code=400, detail="Quantity must be greater than zero") + item = db.query(models.Item).filter(models.Item.barcode == op.barcode).first() if not item: raise HTTPException(status_code=404, detail="Item not found. Register item first.") - + # Update quantity item.quantity += op.quantity - - # Create Mandatory Audit Log + + # Create Mandatory Audit Log — [M-02] user_id din token audit = models.AuditLog( - user_id=op.user_id, + user_id=current_user.sub, action="CHECK_IN", target_item_id=item.id, quantity_change=op.quantity ) - + db.add(audit) db.commit() db.refresh(item) return item @router.post("/check-out", response_model=schemas.Item) -def check_out_item(op: schemas.OperationCreate, db: Session = Depends(get_db)): +def check_out_item( + op: schemas.OperationCreate, + db: Session = Depends(get_db), + current_user: auth.TokenData = Depends(auth.get_current_user) +): + """[C-01] Check-out item — doar utilizatori autentificati.""" if op.quantity <= 0: - raise HTTPException(status_code=400, detail="Quantity must be greater than zero") - + raise HTTPException(status_code=400, detail="Quantity must be greater than zero") + item = db.query(models.Item).filter(models.Item.barcode == op.barcode).first() if not item: raise HTTPException(status_code=404, detail="Item not found") - + if item.quantity < op.quantity: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Insufficient stock") - + # Update quantity item.quantity -= op.quantity - + # Create Mandatory Audit Log audit = models.AuditLog( - user_id=op.user_id, + user_id=current_user.sub, action="CHECK_OUT", target_item_id=item.id, quantity_change=-op.quantity ) - + db.add(audit) db.commit() db.refresh(item) return item @router.post("/trash", response_model=schemas.Item) -def trash_item(op: schemas.TrashOperationCreate, db: Session = Depends(get_db)): +def trash_item( + op: schemas.TrashOperationCreate, + db: Session = Depends(get_db), + current_user: auth.TokenData = Depends(auth.get_current_user) +): + """[C-01] Trash item — doar utilizatori autentificati.""" if op.quantity <= 0: - raise HTTPException(status_code=400, detail="Quantity must be greater than zero") - + raise HTTPException(status_code=400, detail="Quantity must be greater than zero") + item = db.query(models.Item).filter(models.Item.barcode == op.barcode).first() if not item: raise HTTPException(status_code=404, detail="Item not found") - + if item.quantity < op.quantity: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Insufficient stock to trash") - + # Update quantity item.quantity -= op.quantity - + # Create Mandatory Audit Log with TRASH action and reason in details audit = models.AuditLog( - user_id=op.user_id, + user_id=current_user.sub, action="TRASH", target_item_id=item.id, quantity_change=-op.quantity, details=op.reason ) - + db.add(audit) db.commit() db.refresh(item) return item @router.post("/bulk-check-out") -def bulk_check_out(bulk_op: schemas.BulkOperationCreate, db: Session = Depends(get_db)): +def bulk_check_out( + bulk_op: schemas.BulkOperationCreate, + db: Session = Depends(get_db), + current_user: auth.TokenData = Depends(auth.get_current_user) +): + """[C-01] Bulk check-out — doar utilizatori autentificati.""" results = {"success": [], "errors": []} - + for op in bulk_op.items: try: item = db.query(models.Item).filter(models.Item.barcode == op.barcode).first() if not item: results["errors"].append({"barcode": op.barcode, "error": "Not found"}) continue - + if item.quantity < op.quantity: results["errors"].append({"barcode": op.barcode, "error": f"Insufficient stock (Available: {item.quantity})"}) continue - + # Update quantity item.quantity -= op.quantity - + # Log individual audit for this item audit = models.AuditLog( - user_id=bulk_op.user_id, + user_id=current_user.sub, action="BULK_CHECK_OUT", target_item_id=item.id, quantity_change=-op.quantity ) db.add(audit) results["success"].append({"barcode": op.barcode, "new_quantity": item.quantity}) - + except Exception as e: results["errors"].append({"barcode": op.barcode, "error": str(e)}) - + db.commit() return results @router.post("/bulk-sync") -def bulk_sync(payload: schemas.SyncPayload, db: Session = Depends(get_db)): +def bulk_sync( + payload: schemas.SyncPayload, + db: Session = Depends(get_db), + current_user: auth.TokenData = Depends(auth.get_current_user) +): + """[C-01] Bulk sync offline operations — doar utilizatori autentificati.""" results = {"success": [], "errors": []} - + for op in payload.operations: try: # DEDUPLICATION CHECK: If this UUID already exists, skip it @@ -142,7 +167,7 @@ def bulk_sync(payload: schemas.SyncPayload, db: Session = Depends(get_db)): if not item: results["errors"].append({"barcode": op.barcode, "error": "Item not found"}) continue - + if op.type == "CHECK_IN": item.quantity += op.quantity change = op.quantity @@ -155,10 +180,10 @@ def bulk_sync(payload: schemas.SyncPayload, db: Session = Depends(get_db)): else: results["errors"].append({"barcode": op.barcode, "error": f"Invalid operation type: {op.type}"}) continue - + # Log audit with original offline timestamp and UUID audit = models.AuditLog( - user_id=payload.user_id, + user_id=current_user.sub, action=op.type, target_item_id=item.id, quantity_change=change, @@ -168,15 +193,20 @@ def bulk_sync(payload: schemas.SyncPayload, db: Session = Depends(get_db)): ) db.add(audit) results["success"].append({"barcode": op.barcode, "type": op.type}) - + except Exception as e: results["errors"].append({"barcode": op.barcode, "error": str(e)}) - + db.commit() return results @router.get("/logs", response_model=List[schemas.AuditLogResponse]) -def get_logs(limit: int = 50, db: Session = Depends(get_db)): +def get_logs( + limit: int = 50, + db: Session = Depends(get_db), + current_user: auth.TokenData = Depends(auth.get_current_user) +): + """[C-01] Lista audit logs — doar utilizatori autentificati.""" # Join with User to get the username directly logs_with_users = db.query( models.AuditLog.id, @@ -188,7 +218,7 @@ def get_logs(limit: int = 50, db: Session = Depends(get_db)): models.AuditLog.quantity_change, models.AuditLog.details ).join(models.User, models.AuditLog.user_id == models.User.id).order_by(models.AuditLog.timestamp.desc()).limit(limit).all() - + # Mapper to dictionary for Pydantic return [ { diff --git a/backend/routers/users.py b/backend/routers/users.py index ba297c80..64da8b86 100644 --- a/backend/routers/users.py +++ b/backend/routers/users.py @@ -7,7 +7,7 @@ import ldap3 from ldap3.utils.conv import escape_filter_chars import json import os -from .. import models, schemas, database +from .. import models, schemas, database, auth from ..logger import log router = APIRouter(prefix="/users", tags=["users"]) @@ -110,7 +110,11 @@ def verify_password(plain_password, hashed_password): return pwd_context.verify(plain_password, hashed_password) @router.get("/", response_model=List[schemas.User]) -def get_users(db: Session = Depends(get_db)): +def get_users( + db: Session = Depends(get_db), + current_user: auth.TokenData = Depends(auth.get_current_user) +): + """[C-01] Lista utilizatori — doar pentru useri autentificati.""" users = db.query(models.User).all() # Auto-seed if empty if not users: @@ -130,11 +134,16 @@ def get_users(db: Session = Depends(get_db)): return users @router.post("/", response_model=schemas.User) -def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): +def create_user( + user: schemas.UserCreate, + db: Session = Depends(get_db), + current_user: auth.TokenData = Depends(auth.get_current_admin) +): + """[C-01] Creare utilizator — doar admin.""" existing = db.query(models.User).filter(models.User.username == user.username).first() if existing: raise HTTPException(status_code=400, detail="Username already exists") - + hashed = get_password_hash(user.password) if user.password else None new_user = models.User(username=user.username, role=user.role, origin="local", hashed_password=hashed) db.add(new_user) @@ -142,10 +151,13 @@ def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): db.refresh(new_user) return new_user -@router.post("/login") +@router.post("/login", response_model=schemas.TokenResponse) def login(form_data: schemas.UserLogin, db: Session = Depends(get_db)): + """ + [C-01] Login endpoint: validează credențiale și returnează JWT token Bearer. + """ user = db.query(models.User).filter(models.User.username == form_data.username).first() - + # Try local authentication authenticated = False if user and user.hashed_password: @@ -155,7 +167,7 @@ def login(form_data: schemas.UserLogin, db: Session = Depends(get_db)): # [SECURITY FIX C-02] Bypass-ul pentru utilizatori fără parolă a fost eliminat. # Utilizatorii LDAP trebuie să se autentifice prin fluxul LDAP de mai jos. pass - + # If local failed, try LDAP if not authenticated: ldap_role = authenticate_ldap(form_data.username, form_data.password) @@ -163,12 +175,12 @@ def login(form_data: schemas.UserLogin, db: Session = Depends(get_db)): authenticated = True # Cache hash for offline support new_hash = get_password_hash(form_data.password) - + # If user doesn't exist locally, create a stub for role management if not user: user = models.User( - username=form_data.username, - role=ldap_role, + username=form_data.username, + role=ldap_role, origin="ldap", hashed_password=new_hash ) @@ -182,49 +194,79 @@ def login(form_data: schemas.UserLogin, db: Session = Depends(get_db)): db.commit() db.refresh(user) else: - raise HTTPException(status_code=400, detail="Invalid username or password, or insufficient permissions") - - return user + raise HTTPException(status_code=401, detail="Invalid username or password, or insufficient permissions") + + if not authenticated or not user: + raise HTTPException(status_code=401, detail="Invalid username or password") + + # [C-01] Generare JWT token + token = auth.create_access_token( + user_id=user.id, + username=user.username, + role=user.role + ) + + return schemas.TokenResponse( + access_token=token, + token_type="bearer", + user_id=user.id, + username=user.username, + role=user.role + ) @router.put("/{user_id}", response_model=schemas.User) -def update_user(user_id: int, user_update: schemas.UserUpdate, db: Session = Depends(get_db)): +def update_user( + user_id: int, + user_update: schemas.UserUpdate, + db: Session = Depends(get_db), + current_user: auth.TokenData = Depends(auth.get_current_admin) +): + """[C-01] Actualizare utilizator — doar admin.""" db_user = db.query(models.User).filter(models.User.id == user_id).first() if not db_user: raise HTTPException(status_code=404, detail="User not found") - + if user_update.username and db_user.username == "Admin" and user_update.username != "Admin": - raise HTTPException(status_code=400, detail="Cannot change Admin username") - + raise HTTPException(status_code=400, detail="Cannot change Admin username") + if user_update.username: # Check if username already taken by another user existing = db.query(models.User).filter(models.User.username == user_update.username, models.User.id != user_id).first() if existing: raise HTTPException(status_code=400, detail="Username already exists") db_user.username = user_update.username - + if user_update.password: db_user.hashed_password = get_password_hash(user_update.password) - + if user_update.role: db_user.role = user_update.role - + db.commit() db.refresh(db_user) return db_user @router.get("/ldap-config") -def get_ldap_settings(): +def get_ldap_settings(current_user: auth.TokenData = Depends(auth.get_current_admin)): + """[C-01] Obține config LDAP — doar admin.""" return get_ldap_config() @router.post("/ldap-config") -def update_ldap_settings(config: dict): +def update_ldap_settings( + config: dict, + current_user: auth.TokenData = Depends(auth.get_current_admin) +): + """[C-01] Actualizează config LDAP — doar admin.""" config_path = os.path.join(database.DATA_DIR, "ldap_config.json") with open(config_path, "w") as f: json.dump(config, f) return {"message": "Config saved"} @router.post("/test-ldap") -def test_ldap_connection(config: dict): +def test_ldap_connection( + config: dict, + current_user: auth.TokenData = Depends(auth.get_current_admin) +): import socket try: # Extract host and port @@ -273,14 +315,19 @@ def test_ldap_connection(config: dict): return {"status": "error", "message": f"Network Error: {str(e)}"} @router.delete("/{user_id}") -def delete_user(user_id: int, db: Session = Depends(get_db)): +def delete_user( + user_id: int, + db: Session = Depends(get_db), + current_user: auth.TokenData = Depends(auth.get_current_admin) +): + """[C-01] Ștergere utilizator — doar admin.""" user = db.query(models.User).filter(models.User.id == user_id).first() if not user: raise HTTPException(status_code=404, detail="User not found") - + if user.username == "Admin": - raise HTTPException(status_code=400, detail="Cannot delete default Admin") - + raise HTTPException(status_code=400, detail="Cannot delete default Admin") + db.delete(user) db.commit() return {"message": "User deleted"} diff --git a/backend/schemas.py b/backend/schemas.py index 0b49e0f9..0711bbcc 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -30,6 +30,13 @@ class UserPasswordUpdate(BaseModel): old_password: Optional[str] = None new_password: str +class TokenResponse(BaseModel): + access_token: str + token_type: str = "bearer" + user_id: int + username: str + role: str + # --- Categories --- class CategoryBase(BaseModel): name: str