from datetime import datetime, timedelta, timezone from fastapi import Depends, HTTPException, Query from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession import bcrypt from jose import JWTError, jwt from app.config import settings from app.database import get_db from app.models import User security = HTTPBearer() _optional_security = HTTPBearer(auto_error=False) def hash_password(password: str) -> str: return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() def verify_password(plain: str, hashed: str) -> bool: return bcrypt.checkpw(plain.encode(), hashed.encode()) def create_access_token(data: dict) -> str: to_encode = data.copy() expire = datetime.now(timezone.utc) + timedelta(days=settings.access_token_expire_days) to_encode.update({"exp": expire}) return jwt.encode(to_encode, settings.secret_key, algorithm="HS256") def decode_access_token(token: str) -> dict: try: return jwt.decode(token, settings.secret_key, algorithms=["HS256"]) except JWTError: raise HTTPException(401, "Invalid or expired token") async def _resolve_user(token_str: str, db: AsyncSession) -> User: payload = decode_access_token(token_str) user_id_str: str = payload.get("sub") if user_id_str is None: raise HTTPException(401, "Invalid token payload") try: user_id = int(user_id_str) except (ValueError, TypeError): raise HTTPException(401, "Invalid token payload") result = await db.execute(select(User).where(User.id == user_id)) user = result.scalars().first() if not user: raise HTTPException(401, "User not found") return user async def get_current_user( credentials: HTTPAuthorizationCredentials = Depends(security), db: AsyncSession = Depends(get_db), ) -> User: return await _resolve_user(credentials.credentials, db) async def get_current_user_from_query_or_header( token: str | None = Query(default=None), credentials: HTTPAuthorizationCredentials | None = Depends(_optional_security), db: AsyncSession = Depends(get_db), ) -> User: if credentials: token_str = credentials.credentials elif token: token_str = token else: raise HTTPException(401, "Not authenticated") return await _resolve_user(token_str, db) async def get_current_admin_user( current_user: User = Depends(get_current_user), ) -> User: if not current_user.is_admin: raise HTTPException(403, "Admin privileges required") return current_user