Files
simple-pos-system/local_backend/routers/auth.py
bonamin 24a029a8cc Fix order saving, isMyOrder, blocked waiters, options pricing; add preferences, table groups, product images
Backend:
- OrderItemInput accepts option objects {id,name,price_delta} instead of int IDs
- extra_cost from selected options added to unit_price snapshot
- GET /api/products/?all=true for manager (includes unavailable)
- PUT /api/products/{id} now replaces options, ingredients, preference_sets
- POST /api/products/{id}/image — persistent image upload to /app/data/product_images
- New models: ProductPreferenceSet, ProductPreferenceChoice, TableGroup
- tables: group_id FK, hard delete (?hard=true), batch create POST /api/tables/batch
- GET /api/tables/groups + POST/PUT/DELETE groups endpoints
- POST /api/auth/me endpoint for token rehydration
- Auto-migration on startup for new columns

PWA:
- AuthRehydrator: fetches /auth/me on load so isMyOrder works after page reload
- 401 response force-logs out (covers blocked waiters)
- ItemOptionsModal: uses extra_cost correctly, shows preferences as radio buttons

Manager:
- ProductsPage: shows unavailable products greyed out, category color picker + reorder,
  full option/ingredient/preference editing, image upload
- TablesPage: table groups, auto-increment, deactivate vs hard delete, batch add
2026-04-20 18:39:51 +03:00

70 lines
2.5 KiB
Python

import jwt
import bcrypt
from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from database import get_db
from config import settings
from models.user import User
from schemas.auth import LoginRequest, TokenResponse
from schemas.user import UserOut
router = APIRouter()
TOKEN_EXPIRY_HOURS = 8
# In-memory token blacklist (cleared on restart — acceptable for local use)
_blacklisted_tokens: set[str] = set()
def _make_token(user: User) -> str:
payload = {
"sub": str(user.id),
"username": user.username,
"role": user.role,
"exp": datetime.now(timezone.utc) + timedelta(hours=TOKEN_EXPIRY_HOURS),
}
return jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256")
def decode_token(token: str) -> dict:
if token in _blacklisted_tokens:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token revoked")
try:
return jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired")
except jwt.InvalidTokenError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
@router.post("/login", response_model=TokenResponse)
def login(body: LoginRequest, db: Session = Depends(get_db)):
user = db.query(User).filter(User.username == body.username, User.is_active == True).first()
if not user or not bcrypt.checkpw(body.pin.encode(), user.pin_hash.encode()):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
token = _make_token(user)
return TokenResponse(access_token=token, user=UserOut.model_validate(user))
@router.post("/refresh", response_model=TokenResponse)
def refresh(token: str, db: Session = Depends(get_db)):
payload = decode_token(token)
user = db.query(User).filter(User.id == int(payload["sub"]), User.is_active == True).first()
if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
_blacklisted_tokens.add(token)
new_token = _make_token(user)
return TokenResponse(access_token=new_token, user=UserOut.model_validate(user))
@router.post("/logout")
def logout(token: str):
_blacklisted_tokens.add(token)
return {"status": "logged out"}
@router.get("/me", response_model=UserOut)
def me(db: Session = Depends(get_db), user: User = Depends(get_current_user)):
return user