Files
bambuddy/backend/app/api/routes/auth.py
T
2026-01-21 14:50:31 +01:00

214 lines
7.8 KiB
Python

from datetime import timedelta
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.core.auth import (
ACCESS_TOKEN_EXPIRE_MINUTES,
authenticate_user,
create_access_token,
get_current_active_user,
get_password_hash,
get_user_by_username,
)
from backend.app.core.database import get_db
from backend.app.models.settings import Settings
from backend.app.models.user import User
from backend.app.schemas.auth import LoginRequest, LoginResponse, SetupRequest, SetupResponse, UserResponse
router = APIRouter(prefix="/auth", tags=["authentication"])
async def is_auth_enabled(db: AsyncSession) -> bool:
"""Check if authentication is enabled."""
result = await db.execute(select(Settings).where(Settings.key == "auth_enabled"))
setting = result.scalar_one_or_none()
return setting and setting.value.lower() == "true"
async def set_auth_enabled(db: AsyncSession, enabled: bool) -> None:
"""Set authentication enabled status."""
from sqlalchemy import func
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
stmt = sqlite_insert(Settings).values(key="auth_enabled", value="true" if enabled else "false")
stmt = stmt.on_conflict_do_update(
index_elements=["key"], set_={"value": "true" if enabled else "false", "updated_at": func.now()}
)
await db.execute(stmt)
# Note: Don't commit here - let get_db handle it or commit explicitly in the route
@router.post("/setup", response_model=SetupResponse)
async def setup_auth(request: SetupRequest, db: AsyncSession = Depends(get_db)):
"""First-time setup: enable/disable authentication and create admin user."""
import logging
logger = logging.getLogger(__name__)
try:
# Check if auth is already configured (prevent re-setup)
result = await db.execute(select(Settings).where(Settings.key == "auth_enabled"))
existing_setting = result.scalar_one_or_none()
# Check if users exist
user_count_result = await db.execute(select(User))
user_count = len(user_count_result.scalars().all())
if existing_setting and user_count > 0:
# Auth already configured and users exist - prevent re-setup
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Authentication is already configured. Use user management to modify users.",
)
# If auth_enabled is true but no users exist, allow re-setup (recovery scenario)
admin_created = False
if request.auth_enabled:
if not request.admin_username or not request.admin_password:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Admin username and password are required when enabling authentication",
)
# Check if admin already exists
existing_admin = await get_user_by_username(db, request.admin_username)
if existing_admin:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Admin user already exists",
)
# Create admin user FIRST (before enabling auth)
try:
logger.info(f"Creating admin user: {request.admin_username}")
admin_user = User(
username=request.admin_username,
password_hash=get_password_hash(request.admin_password),
role="admin",
is_active=True,
)
db.add(admin_user)
logger.info(f"Admin user added to session: {request.admin_username}")
admin_created = True
except Exception as e:
await db.rollback()
logger.error(f"Failed to create admin user: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to create admin user: {str(e)}",
)
# Set auth enabled and commit everything together
await set_auth_enabled(db, request.auth_enabled)
await db.commit()
if admin_created:
await db.refresh(admin_user)
logger.info(f"Admin user created successfully: {admin_user.id}")
return SetupResponse(auth_enabled=request.auth_enabled, admin_created=admin_created)
except HTTPException:
raise
except Exception as e:
logger.error(f"Setup error: {e}", exc_info=True)
await db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Setup failed: {str(e)}",
)
@router.get("/status")
async def get_auth_status(db: AsyncSession = Depends(get_db)):
"""Get authentication status (public endpoint)."""
auth_enabled = await is_auth_enabled(db)
return {"auth_enabled": auth_enabled, "requires_setup": not auth_enabled}
@router.post("/disable", response_model=dict)
async def disable_auth(
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
"""Disable authentication (admin only)."""
import logging
logger = logging.getLogger(__name__)
# Only admins can disable authentication
if current_user.role != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only admins can disable authentication",
)
try:
await set_auth_enabled(db, False)
await db.commit()
logger.info(f"Authentication disabled by admin user: {current_user.username}")
return {"message": "Authentication disabled successfully", "auth_enabled": False}
except Exception as e:
await db.rollback()
logger.error(f"Failed to disable authentication: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to disable authentication: {str(e)}",
)
@router.post("/login", response_model=LoginResponse)
async def login(request: LoginRequest, db: AsyncSession = Depends(get_db)):
"""Login and get access token."""
# Check if auth is enabled
auth_enabled = await is_auth_enabled(db)
if not auth_enabled:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Authentication is not enabled",
)
user = await authenticate_user(db, request.username, request.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(data={"sub": user.username}, expires_delta=access_token_expires)
return LoginResponse(
access_token=access_token,
token_type="bearer",
user=UserResponse(
id=user.id,
username=user.username,
role=user.role,
is_active=user.is_active,
created_at=user.created_at.isoformat(),
),
)
@router.get("/me", response_model=UserResponse)
async def get_current_user_info(current_user: User = Depends(get_current_active_user)):
"""Get current user information."""
return UserResponse(
id=current_user.id,
username=current_user.username,
role=current_user.role,
is_active=current_user.is_active,
created_at=current_user.created_at.isoformat(),
)
@router.post("/logout")
async def logout():
"""Logout (client should discard token)."""
return {"message": "Logged out successfully"}