initial
This commit is contained in:
83
backend/app/api/auth.py
Normal file
83
backend/app/api/auth.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""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)):
|
||||
result = await db.execute(select(User).where(User.email == payload.email))
|
||||
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))
|
||||
Reference in New Issue
Block a user