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))
|