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

View File

@@ -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

View File

@@ -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"}

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"}

View File

@@ -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 [
{

View File

@@ -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"}

View File

@@ -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