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:
100
backend/auth.py
Normal file
100
backend/auth.py
Normal 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
|
||||||
@@ -10,3 +10,5 @@ Pillow>=10.0.0
|
|||||||
python-multipart>=0.0.9
|
python-multipart>=0.0.9
|
||||||
ldap3>=2.9.1
|
ldap3>=2.9.1
|
||||||
passlib[bcrypt]>=1.7.4
|
passlib[bcrypt]>=1.7.4
|
||||||
|
python-jose[cryptography]>=3.3.0
|
||||||
|
slowapi>=0.1.9
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import List
|
from typing import List
|
||||||
from .. import models, schemas, database
|
from .. import models, schemas, auth, database
|
||||||
|
|
||||||
router = APIRouter(prefix="/categories", tags=["categories"])
|
router = APIRouter(prefix="/categories", tags=["categories"])
|
||||||
|
|
||||||
@@ -13,7 +13,11 @@ def get_db():
|
|||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
@router.get("/", response_model=List[schemas.Category])
|
@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()
|
categories = db.query(models.Category).all()
|
||||||
# Auto-seed if empty with defaults mentioned by user
|
# Auto-seed if empty with defaults mentioned by user
|
||||||
if not categories:
|
if not categories:
|
||||||
@@ -31,11 +35,16 @@ def get_categories(db: Session = Depends(get_db)):
|
|||||||
return categories
|
return categories
|
||||||
|
|
||||||
@router.post("/", response_model=schemas.Category)
|
@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()
|
existing = db.query(models.Category).filter(models.Category.name == category.name).first()
|
||||||
if existing:
|
if existing:
|
||||||
raise HTTPException(status_code=400, detail="Category already exists")
|
raise HTTPException(status_code=400, detail="Category already exists")
|
||||||
|
|
||||||
new_cat = models.Category(**category.model_dump())
|
new_cat = models.Category(**category.model_dump())
|
||||||
db.add(new_cat)
|
db.add(new_cat)
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -43,30 +52,41 @@ def create_category(category: schemas.CategoryCreate, db: Session = Depends(get_
|
|||||||
return new_cat
|
return new_cat
|
||||||
|
|
||||||
@router.put("/{cat_id}", response_model=schemas.Category)
|
@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()
|
db_cat = db.query(models.Category).filter(models.Category.id == cat_id).first()
|
||||||
if not db_cat:
|
if not db_cat:
|
||||||
raise HTTPException(status_code=404, detail="Category not found")
|
raise HTTPException(status_code=404, detail="Category not found")
|
||||||
|
|
||||||
update_data = category.model_dump(exclude_unset=True)
|
update_data = category.model_dump(exclude_unset=True)
|
||||||
for key, value in update_data.items():
|
for key, value in update_data.items():
|
||||||
setattr(db_cat, key, value)
|
setattr(db_cat, key, value)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_cat)
|
db.refresh(db_cat)
|
||||||
return db_cat
|
return db_cat
|
||||||
|
|
||||||
@router.delete("/{cat_id}")
|
@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()
|
cat = db.query(models.Category).filter(models.Category.id == cat_id).first()
|
||||||
if not cat:
|
if not cat:
|
||||||
raise HTTPException(status_code=404, detail="Category not found")
|
raise HTTPException(status_code=404, detail="Category not found")
|
||||||
|
|
||||||
# Check if items are linked
|
# Check if items are linked
|
||||||
linkedItems = db.query(models.Item).filter(models.Item.category_id == cat_id).count()
|
linkedItems = db.query(models.Item).filter(models.Item.category_id == cat_id).count()
|
||||||
if linkedItems > 0:
|
if linkedItems > 0:
|
||||||
raise HTTPException(status_code=400, detail="Cannot delete category with linked items")
|
raise HTTPException(status_code=400, detail="Cannot delete category with linked items")
|
||||||
|
|
||||||
db.delete(cat)
|
db.delete(cat)
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"message": "Category deleted"}
|
return {"message": "Category deleted"}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
from typing import List
|
from typing import List
|
||||||
from .. import models, schemas
|
from .. import models, schemas, auth
|
||||||
from ..database import get_db
|
from ..database import get_db
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
@@ -11,14 +11,18 @@ router = APIRouter(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@router.get("/stats")
|
@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_categories = db.query(models.Category).count()
|
||||||
total_items = db.query(models.Item).count()
|
total_items = db.query(models.Item).count()
|
||||||
|
|
||||||
# Count items per category string
|
# Count items per category string
|
||||||
items_per_category = db.query(models.Item.category, func.count(models.Item.id))\
|
items_per_category = db.query(models.Item.category, func.count(models.Item.id))\
|
||||||
.group_by(models.Item.category).all()
|
.group_by(models.Item.category).all()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"total_categories": total_categories,
|
"total_categories": total_categories,
|
||||||
"total_items": total_items,
|
"total_items": total_items,
|
||||||
@@ -26,12 +30,23 @@ def read_item_stats(db: Session = Depends(get_db)):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@router.get("/", response_model=List[schemas.Item])
|
@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()
|
items = db.query(models.Item).offset(skip).limit(limit).all()
|
||||||
return items
|
return items
|
||||||
|
|
||||||
@router.get("/{item_id}", response_model=schemas.Item)
|
@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()
|
item = db.query(models.Item).filter(models.Item.id == item_id).first()
|
||||||
if item is None:
|
if item is None:
|
||||||
raise HTTPException(status_code=404, detail="Item not found")
|
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
|
_MAX_IMAGE_SIZE = 10 * 1024 * 1024 # 10 MB
|
||||||
|
|
||||||
@router.post("/extract-label")
|
@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
|
from ..ai_vision import extract_label_info
|
||||||
|
|
||||||
# [SECURITY FIX H-03] Validare tip MIME și dimensiune maximă
|
# [SECURITY FIX H-03] Validare tip MIME și dimensiune maximă
|
||||||
@@ -63,49 +82,65 @@ async def extract_label(file: UploadFile = File(...)):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
@router.post("/", response_model=schemas.Item, status_code=status.HTTP_201_CREATED)
|
@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
|
# Check if barcode exists
|
||||||
db_item = db.query(models.Item).filter(models.Item.barcode == item.barcode).first()
|
db_item = db.query(models.Item).filter(models.Item.barcode == item.barcode).first()
|
||||||
if db_item:
|
if db_item:
|
||||||
raise HTTPException(status_code=400, detail="Barcode already registered")
|
raise HTTPException(status_code=400, detail="Barcode already registered")
|
||||||
|
|
||||||
db_item = models.Item(**item.model_dump())
|
db_item = models.Item(**item.model_dump())
|
||||||
db.add(db_item)
|
db.add(db_item)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_item)
|
db.refresh(db_item)
|
||||||
|
|
||||||
# Audit log the creation
|
# Audit log the creation — [M-02] user_id din token, nu din body
|
||||||
audit = models.AuditLog(
|
audit = models.AuditLog(
|
||||||
user_id=user_id,
|
user_id=current_user.sub,
|
||||||
action="CREATE_ITEM",
|
action="CREATE_ITEM",
|
||||||
target_item_id=db_item.id,
|
target_item_id=db_item.id,
|
||||||
quantity_change=item.quantity
|
quantity_change=item.quantity
|
||||||
)
|
)
|
||||||
db.add(audit)
|
db.add(audit)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
return db_item
|
return db_item
|
||||||
|
|
||||||
@router.put("/{item_id}", response_model=schemas.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()
|
db_item = db.query(models.Item).filter(models.Item.id == item_id).first()
|
||||||
if not db_item:
|
if not db_item:
|
||||||
raise HTTPException(status_code=404, detail="Item not found")
|
raise HTTPException(status_code=404, detail="Item not found")
|
||||||
|
|
||||||
update_data = item.model_dump(exclude_unset=True)
|
update_data = item.model_dump(exclude_unset=True)
|
||||||
for key, value in update_data.items():
|
for key, value in update_data.items():
|
||||||
setattr(db_item, key, value)
|
setattr(db_item, key, value)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_item)
|
db.refresh(db_item)
|
||||||
return db_item
|
return db_item
|
||||||
|
|
||||||
@router.delete("/{item_id}")
|
@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()
|
db_item = db.query(models.Item).filter(models.Item.id == item_id).first()
|
||||||
if not db_item:
|
if not db_item:
|
||||||
raise HTTPException(status_code=404, detail="Item not found")
|
raise HTTPException(status_code=404, detail="Item not found")
|
||||||
|
|
||||||
db.delete(db_item)
|
db.delete(db_item)
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"message": "Item deleted successfully"}
|
return {"message": "Item deleted successfully"}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from typing import List
|
from typing import List
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from .. import models, schemas
|
from .. import models, schemas, auth
|
||||||
from ..database import get_db
|
from ..database import get_db
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
@@ -10,125 +10,150 @@ router = APIRouter(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@router.post("/check-in", response_model=schemas.Item)
|
@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:
|
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()
|
item = db.query(models.Item).filter(models.Item.barcode == op.barcode).first()
|
||||||
if not item:
|
if not item:
|
||||||
raise HTTPException(status_code=404, detail="Item not found. Register item first.")
|
raise HTTPException(status_code=404, detail="Item not found. Register item first.")
|
||||||
|
|
||||||
# Update quantity
|
# Update quantity
|
||||||
item.quantity += op.quantity
|
item.quantity += op.quantity
|
||||||
|
|
||||||
# Create Mandatory Audit Log
|
# Create Mandatory Audit Log — [M-02] user_id din token
|
||||||
audit = models.AuditLog(
|
audit = models.AuditLog(
|
||||||
user_id=op.user_id,
|
user_id=current_user.sub,
|
||||||
action="CHECK_IN",
|
action="CHECK_IN",
|
||||||
target_item_id=item.id,
|
target_item_id=item.id,
|
||||||
quantity_change=op.quantity
|
quantity_change=op.quantity
|
||||||
)
|
)
|
||||||
|
|
||||||
db.add(audit)
|
db.add(audit)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(item)
|
db.refresh(item)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
@router.post("/check-out", response_model=schemas.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:
|
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()
|
item = db.query(models.Item).filter(models.Item.barcode == op.barcode).first()
|
||||||
if not item:
|
if not item:
|
||||||
raise HTTPException(status_code=404, detail="Item not found")
|
raise HTTPException(status_code=404, detail="Item not found")
|
||||||
|
|
||||||
if item.quantity < op.quantity:
|
if item.quantity < op.quantity:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Insufficient stock")
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Insufficient stock")
|
||||||
|
|
||||||
# Update quantity
|
# Update quantity
|
||||||
item.quantity -= op.quantity
|
item.quantity -= op.quantity
|
||||||
|
|
||||||
# Create Mandatory Audit Log
|
# Create Mandatory Audit Log
|
||||||
audit = models.AuditLog(
|
audit = models.AuditLog(
|
||||||
user_id=op.user_id,
|
user_id=current_user.sub,
|
||||||
action="CHECK_OUT",
|
action="CHECK_OUT",
|
||||||
target_item_id=item.id,
|
target_item_id=item.id,
|
||||||
quantity_change=-op.quantity
|
quantity_change=-op.quantity
|
||||||
)
|
)
|
||||||
|
|
||||||
db.add(audit)
|
db.add(audit)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(item)
|
db.refresh(item)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
@router.post("/trash", response_model=schemas.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:
|
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()
|
item = db.query(models.Item).filter(models.Item.barcode == op.barcode).first()
|
||||||
if not item:
|
if not item:
|
||||||
raise HTTPException(status_code=404, detail="Item not found")
|
raise HTTPException(status_code=404, detail="Item not found")
|
||||||
|
|
||||||
if item.quantity < op.quantity:
|
if item.quantity < op.quantity:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Insufficient stock to trash")
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Insufficient stock to trash")
|
||||||
|
|
||||||
# Update quantity
|
# Update quantity
|
||||||
item.quantity -= op.quantity
|
item.quantity -= op.quantity
|
||||||
|
|
||||||
# Create Mandatory Audit Log with TRASH action and reason in details
|
# Create Mandatory Audit Log with TRASH action and reason in details
|
||||||
audit = models.AuditLog(
|
audit = models.AuditLog(
|
||||||
user_id=op.user_id,
|
user_id=current_user.sub,
|
||||||
action="TRASH",
|
action="TRASH",
|
||||||
target_item_id=item.id,
|
target_item_id=item.id,
|
||||||
quantity_change=-op.quantity,
|
quantity_change=-op.quantity,
|
||||||
details=op.reason
|
details=op.reason
|
||||||
)
|
)
|
||||||
|
|
||||||
db.add(audit)
|
db.add(audit)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(item)
|
db.refresh(item)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
@router.post("/bulk-check-out")
|
@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": []}
|
results = {"success": [], "errors": []}
|
||||||
|
|
||||||
for op in bulk_op.items:
|
for op in bulk_op.items:
|
||||||
try:
|
try:
|
||||||
item = db.query(models.Item).filter(models.Item.barcode == op.barcode).first()
|
item = db.query(models.Item).filter(models.Item.barcode == op.barcode).first()
|
||||||
if not item:
|
if not item:
|
||||||
results["errors"].append({"barcode": op.barcode, "error": "Not found"})
|
results["errors"].append({"barcode": op.barcode, "error": "Not found"})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if item.quantity < op.quantity:
|
if item.quantity < op.quantity:
|
||||||
results["errors"].append({"barcode": op.barcode, "error": f"Insufficient stock (Available: {item.quantity})"})
|
results["errors"].append({"barcode": op.barcode, "error": f"Insufficient stock (Available: {item.quantity})"})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Update quantity
|
# Update quantity
|
||||||
item.quantity -= op.quantity
|
item.quantity -= op.quantity
|
||||||
|
|
||||||
# Log individual audit for this item
|
# Log individual audit for this item
|
||||||
audit = models.AuditLog(
|
audit = models.AuditLog(
|
||||||
user_id=bulk_op.user_id,
|
user_id=current_user.sub,
|
||||||
action="BULK_CHECK_OUT",
|
action="BULK_CHECK_OUT",
|
||||||
target_item_id=item.id,
|
target_item_id=item.id,
|
||||||
quantity_change=-op.quantity
|
quantity_change=-op.quantity
|
||||||
)
|
)
|
||||||
db.add(audit)
|
db.add(audit)
|
||||||
results["success"].append({"barcode": op.barcode, "new_quantity": item.quantity})
|
results["success"].append({"barcode": op.barcode, "new_quantity": item.quantity})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results["errors"].append({"barcode": op.barcode, "error": str(e)})
|
results["errors"].append({"barcode": op.barcode, "error": str(e)})
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@router.post("/bulk-sync")
|
@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": []}
|
results = {"success": [], "errors": []}
|
||||||
|
|
||||||
for op in payload.operations:
|
for op in payload.operations:
|
||||||
try:
|
try:
|
||||||
# DEDUPLICATION CHECK: If this UUID already exists, skip it
|
# 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:
|
if not item:
|
||||||
results["errors"].append({"barcode": op.barcode, "error": "Item not found"})
|
results["errors"].append({"barcode": op.barcode, "error": "Item not found"})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if op.type == "CHECK_IN":
|
if op.type == "CHECK_IN":
|
||||||
item.quantity += op.quantity
|
item.quantity += op.quantity
|
||||||
change = op.quantity
|
change = op.quantity
|
||||||
@@ -155,10 +180,10 @@ def bulk_sync(payload: schemas.SyncPayload, db: Session = Depends(get_db)):
|
|||||||
else:
|
else:
|
||||||
results["errors"].append({"barcode": op.barcode, "error": f"Invalid operation type: {op.type}"})
|
results["errors"].append({"barcode": op.barcode, "error": f"Invalid operation type: {op.type}"})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Log audit with original offline timestamp and UUID
|
# Log audit with original offline timestamp and UUID
|
||||||
audit = models.AuditLog(
|
audit = models.AuditLog(
|
||||||
user_id=payload.user_id,
|
user_id=current_user.sub,
|
||||||
action=op.type,
|
action=op.type,
|
||||||
target_item_id=item.id,
|
target_item_id=item.id,
|
||||||
quantity_change=change,
|
quantity_change=change,
|
||||||
@@ -168,15 +193,20 @@ def bulk_sync(payload: schemas.SyncPayload, db: Session = Depends(get_db)):
|
|||||||
)
|
)
|
||||||
db.add(audit)
|
db.add(audit)
|
||||||
results["success"].append({"barcode": op.barcode, "type": op.type})
|
results["success"].append({"barcode": op.barcode, "type": op.type})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results["errors"].append({"barcode": op.barcode, "error": str(e)})
|
results["errors"].append({"barcode": op.barcode, "error": str(e)})
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@router.get("/logs", response_model=List[schemas.AuditLogResponse])
|
@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
|
# Join with User to get the username directly
|
||||||
logs_with_users = db.query(
|
logs_with_users = db.query(
|
||||||
models.AuditLog.id,
|
models.AuditLog.id,
|
||||||
@@ -188,7 +218,7 @@ def get_logs(limit: int = 50, db: Session = Depends(get_db)):
|
|||||||
models.AuditLog.quantity_change,
|
models.AuditLog.quantity_change,
|
||||||
models.AuditLog.details
|
models.AuditLog.details
|
||||||
).join(models.User, models.AuditLog.user_id == models.User.id).order_by(models.AuditLog.timestamp.desc()).limit(limit).all()
|
).join(models.User, models.AuditLog.user_id == models.User.id).order_by(models.AuditLog.timestamp.desc()).limit(limit).all()
|
||||||
|
|
||||||
# Mapper to dictionary for Pydantic
|
# Mapper to dictionary for Pydantic
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import ldap3
|
|||||||
from ldap3.utils.conv import escape_filter_chars
|
from ldap3.utils.conv import escape_filter_chars
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from .. import models, schemas, database
|
from .. import models, schemas, database, auth
|
||||||
from ..logger import log
|
from ..logger import log
|
||||||
|
|
||||||
router = APIRouter(prefix="/users", tags=["users"])
|
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)
|
return pwd_context.verify(plain_password, hashed_password)
|
||||||
|
|
||||||
@router.get("/", response_model=List[schemas.User])
|
@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()
|
users = db.query(models.User).all()
|
||||||
# Auto-seed if empty
|
# Auto-seed if empty
|
||||||
if not users:
|
if not users:
|
||||||
@@ -130,11 +134,16 @@ def get_users(db: Session = Depends(get_db)):
|
|||||||
return users
|
return users
|
||||||
|
|
||||||
@router.post("/", response_model=schemas.User)
|
@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()
|
existing = db.query(models.User).filter(models.User.username == user.username).first()
|
||||||
if existing:
|
if existing:
|
||||||
raise HTTPException(status_code=400, detail="Username already exists")
|
raise HTTPException(status_code=400, detail="Username already exists")
|
||||||
|
|
||||||
hashed = get_password_hash(user.password) if user.password else None
|
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)
|
new_user = models.User(username=user.username, role=user.role, origin="local", hashed_password=hashed)
|
||||||
db.add(new_user)
|
db.add(new_user)
|
||||||
@@ -142,10 +151,13 @@ def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
|
|||||||
db.refresh(new_user)
|
db.refresh(new_user)
|
||||||
return 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)):
|
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()
|
user = db.query(models.User).filter(models.User.username == form_data.username).first()
|
||||||
|
|
||||||
# Try local authentication
|
# Try local authentication
|
||||||
authenticated = False
|
authenticated = False
|
||||||
if user and user.hashed_password:
|
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.
|
# [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.
|
# Utilizatorii LDAP trebuie să se autentifice prin fluxul LDAP de mai jos.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# If local failed, try LDAP
|
# If local failed, try LDAP
|
||||||
if not authenticated:
|
if not authenticated:
|
||||||
ldap_role = authenticate_ldap(form_data.username, form_data.password)
|
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
|
authenticated = True
|
||||||
# Cache hash for offline support
|
# Cache hash for offline support
|
||||||
new_hash = get_password_hash(form_data.password)
|
new_hash = get_password_hash(form_data.password)
|
||||||
|
|
||||||
# If user doesn't exist locally, create a stub for role management
|
# If user doesn't exist locally, create a stub for role management
|
||||||
if not user:
|
if not user:
|
||||||
user = models.User(
|
user = models.User(
|
||||||
username=form_data.username,
|
username=form_data.username,
|
||||||
role=ldap_role,
|
role=ldap_role,
|
||||||
origin="ldap",
|
origin="ldap",
|
||||||
hashed_password=new_hash
|
hashed_password=new_hash
|
||||||
)
|
)
|
||||||
@@ -182,49 +194,79 @@ def login(form_data: schemas.UserLogin, db: Session = Depends(get_db)):
|
|||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(user)
|
db.refresh(user)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=400, detail="Invalid username or password, or insufficient permissions")
|
raise HTTPException(status_code=401, detail="Invalid username or password, or insufficient permissions")
|
||||||
|
|
||||||
return user
|
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)
|
@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()
|
db_user = db.query(models.User).filter(models.User.id == user_id).first()
|
||||||
if not db_user:
|
if not db_user:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|
||||||
if user_update.username and db_user.username == "Admin" and user_update.username != "Admin":
|
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:
|
if user_update.username:
|
||||||
# Check if username already taken by another user
|
# 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()
|
existing = db.query(models.User).filter(models.User.username == user_update.username, models.User.id != user_id).first()
|
||||||
if existing:
|
if existing:
|
||||||
raise HTTPException(status_code=400, detail="Username already exists")
|
raise HTTPException(status_code=400, detail="Username already exists")
|
||||||
db_user.username = user_update.username
|
db_user.username = user_update.username
|
||||||
|
|
||||||
if user_update.password:
|
if user_update.password:
|
||||||
db_user.hashed_password = get_password_hash(user_update.password)
|
db_user.hashed_password = get_password_hash(user_update.password)
|
||||||
|
|
||||||
if user_update.role:
|
if user_update.role:
|
||||||
db_user.role = user_update.role
|
db_user.role = user_update.role
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_user)
|
db.refresh(db_user)
|
||||||
return db_user
|
return db_user
|
||||||
|
|
||||||
@router.get("/ldap-config")
|
@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()
|
return get_ldap_config()
|
||||||
|
|
||||||
@router.post("/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")
|
config_path = os.path.join(database.DATA_DIR, "ldap_config.json")
|
||||||
with open(config_path, "w") as f:
|
with open(config_path, "w") as f:
|
||||||
json.dump(config, f)
|
json.dump(config, f)
|
||||||
return {"message": "Config saved"}
|
return {"message": "Config saved"}
|
||||||
|
|
||||||
@router.post("/test-ldap")
|
@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
|
import socket
|
||||||
try:
|
try:
|
||||||
# Extract host and port
|
# Extract host and port
|
||||||
@@ -273,14 +315,19 @@ def test_ldap_connection(config: dict):
|
|||||||
return {"status": "error", "message": f"Network Error: {str(e)}"}
|
return {"status": "error", "message": f"Network Error: {str(e)}"}
|
||||||
|
|
||||||
@router.delete("/{user_id}")
|
@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()
|
user = db.query(models.User).filter(models.User.id == user_id).first()
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|
||||||
if user.username == "Admin":
|
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.delete(user)
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"message": "User deleted"}
|
return {"message": "User deleted"}
|
||||||
|
|||||||
@@ -30,6 +30,13 @@ class UserPasswordUpdate(BaseModel):
|
|||||||
old_password: Optional[str] = None
|
old_password: Optional[str] = None
|
||||||
new_password: str
|
new_password: str
|
||||||
|
|
||||||
|
class TokenResponse(BaseModel):
|
||||||
|
access_token: str
|
||||||
|
token_type: str = "bearer"
|
||||||
|
user_id: int
|
||||||
|
username: str
|
||||||
|
role: str
|
||||||
|
|
||||||
# --- Categories ---
|
# --- Categories ---
|
||||||
class CategoryBase(BaseModel):
|
class CategoryBase(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
|
|||||||
Reference in New Issue
Block a user