from datetime import datetime, timedelta from typing import Optional from jose import JWTError, jwt from passlib.context import CryptContext from fastapi import Depends, HTTPException, status, Request from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session from database import get_async_db, get_db, User import os SECRET_KEY = os.getenv("SECRET_KEY", "supersecretkey-change-in-production") ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") def decode_token(token: str) -> Optional[dict]: if not token: return None try: return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) except JWTError: return None def verify_password(plain_password: str, hashed_password: str) -> bool: return pwd_context.verify(plain_password, hashed_password) def get_password_hash(password: str) -> str: return pwd_context.hash(password) def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: to_encode = data.copy() expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)) to_encode.update({"exp": expire}) return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) async def get_current_user_from_token(token: str, db) -> Optional[User]: if not token: return None payload = decode_token(token) if not payload: return None username = payload.get("sub") if not username: return None if isinstance(db, AsyncSession): result = await db.execute(select(User).where(User.username == username)) return result.scalar_one_or_none() return db.query(User).filter(User.username == username).first() async def get_current_user_optional(request: Request, db=Depends(get_async_db)) -> Optional[User]: token = request.cookies.get("access_token") if not token: return None if token.startswith("Bearer "): token = token[7:] return await get_current_user_from_token(token, db) async def get_current_user(request: Request, db=Depends(get_async_db)) -> User: token = request.cookies.get("access_token") if not token: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated") if token.startswith("Bearer "): token = token[7:] user = await get_current_user_from_token(token, db) if user is None: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials") return user async def get_current_admin(request: Request, db=Depends(get_async_db)) -> User: user = await get_current_user(request, db) if not user.is_admin: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required") return user # ─── Sync auth deps (for non-converted endpoints) ────────────────────────── async def get_current_user_sync(request: Request, db=Depends(get_db)) -> User: token = request.cookies.get("access_token") if not token: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated") if token.startswith("Bearer "): token = token[7:] user = db.query(User).filter(User.username == decode_token(token).get("sub")).first() if decode_token(token) else None if user is None: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials") return user async def get_current_user_optional_sync(request: Request, db=Depends(get_db)) -> Optional[User]: token = request.cookies.get("access_token") if not token: return None if token.startswith("Bearer "): token = token[7:] payload = decode_token(token) if not payload: return None return db.query(User).filter(User.username == payload.get("sub")).first() def create_user(db: Session, username: str, password: str) -> User: hashed = get_password_hash(password) user = User(username=username, hashed_password=hashed, balance=1000.0) db.add(user) db.commit() db.refresh(user) return user def authenticate_user(db: Session, username: str, password: str) -> Optional[User]: user = db.query(User).filter(User.username == username).first() if not user or not verify_password(password, user.hashed_password): return None return user def is_item_craftable(item_id: int) -> bool: from backend import get_item, is_final_in_collection item = get_item(item_id) if not item: return False if not item.get("is_craftable", True): return False if is_final_in_collection(item): return False return True