Files
ai-rpg/backend/app/api/auth.py

90 lines
3.9 KiB
Python
Raw Normal View History

2026-06-19 11:28:04 +03:00
"""Authentication routes: register, login, me, admin setup."""
from __future__ import annotations
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from fastapi import APIRouter, Depends, HTTPException, status
from app.core.security import create_access_token, hash_password, verify_password
from app.core.settings_service import get_setting
from app.db import get_db_dep
from app.deps import get_current_user
from app.models import User
from app.schemas import AdminSetupRequest, TokenOut, UserLogin, UserOut, UserRegister
router = APIRouter(prefix="/api/auth", tags=["auth"])
@router.post("/register", response_model=TokenOut, status_code=status.HTTP_201_CREATED)
async def register(payload: UserRegister, db: AsyncSession = Depends(get_db_dep)):
existing = await db.execute(select(User).where((User.email == payload.email) | (User.username == payload.username)))
if existing.scalars().first():
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="user_already_exists")
user = User(
email=payload.email,
username=payload.username,
hashed_password=hash_password(payload.password),
is_admin=False,
)
db.add(user)
await db.commit()
await db.refresh(user)
token = create_access_token(subject=str(user.id), extra={"is_admin": user.is_admin})
return TokenOut(access_token=token, user=UserOut.model_validate(user))
@router.post("/login", response_model=TokenOut)
async def login(payload: UserLogin, db: AsyncSession = Depends(get_db_dep)):
2026-06-19 16:31:45 +03:00
# Accept either email or username in the `login` field.
login_value = (payload.login or "").strip()
if not login_value:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="login_required")
result = await db.execute(
select(User).where((User.email == login_value) | (User.username == login_value))
)
2026-06-19 11:28:04 +03:00
user = result.scalars().first()
if not user or not verify_password(payload.password, user.hashed_password):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid_credentials")
if not user.is_active:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="user_disabled")
token = create_access_token(subject=str(user.id), extra={"is_admin": user.is_admin})
return TokenOut(access_token=token, user=UserOut.model_validate(user))
@router.get("/me", response_model=UserOut)
async def me(user: User = Depends(get_current_user)):
return user
@router.post("/admin-setup", response_model=TokenOut)
async def admin_setup(payload: AdminSetupRequest, db: AsyncSession = Depends(get_db_dep)):
"""One-time endpoint to create the first admin user using a setup token."""
# Check if any admin already exists
existing_admins = await db.execute(select(User).where(User.is_admin.is_(True)))
if existing_admins.scalars().first() is not None:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="admin_already_exists")
# Validate setup token (from DB or env)
db_token = await get_setting(db, "admin.setup_token", default=None)
env_token = payload.token # what the user supplied
if not db_token or db_token != env_token:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="invalid_setup_token")
# Check user collision
existing = await db.execute(select(User).where((User.email == payload.email) | (User.username == payload.username)))
if existing.scalars().first():
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="user_already_exists")
user = User(
email=payload.email,
username=payload.username,
hashed_password=hash_password(payload.password),
is_admin=True,
)
db.add(user)
await db.commit()
await db.refresh(user)
token = create_access_token(subject=str(user.id), extra={"is_admin": user.is_admin})
return TokenOut(access_token=token, user=UserOut.model_validate(user))