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"}