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:
@@ -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 [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user