"""Authentication service — register, login, refresh.""" from __future__ import annotations from fastapi import HTTPException, status from jose import JWTError, jwt from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.core.security import ( create_access_token, create_refresh_token, decode_token, hash_password, verify_password, ) from app.models.user import User from app.schemas.auth import RegisterRequest async def register(db: AsyncSession, body: RegisterRequest) -> User: """Create a new user. Raises 409 if email already exists.""" result = await db.execute(select(User).where(User.email == body.email)) if result.scalar_one_or_none(): raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Email already registered") user = User( email=body.email, password_hash=hash_password(body.password), full_name=body.full_name, phone=body.phone, role=body.role, ) db.add(user) await db.flush() await db.refresh(user) return user async def login(db: AsyncSession, email: str, password: str) -> tuple[str, str, User]: """Authenticate and return (access_token, refresh_token, user).""" result = await db.execute(select(User).where(User.email == email)) user = result.scalar_one_or_none() if user is None or not verify_password(password, user.password_hash): raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or password") if not user.active: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Account is inactive") access_token = create_access_token({"sub": str(user.id)}) refresh_token = create_refresh_token({"sub": str(user.id)}) return access_token, refresh_token, user async def refresh_access_token(db: AsyncSession, token: str) -> tuple[str, str]: """Validate a refresh token and issue a new token pair.""" try: payload = decode_token(token) if payload.get("type") != "refresh": raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token type") except HTTPException: raise except Exception: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token") user_id: int = int(payload["sub"]) result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if user is None or not user.active: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found or inactive") new_access = create_access_token({"sub": str(user.id)}) new_refresh = create_refresh_token({"sub": str(user.id)}) return new_access, new_refresh