mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-19 07:33:18 +02:00
Revert "Added optional authentication and user management"
This commit is contained in:
@@ -1,213 +0,0 @@
|
||||
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"}
|
||||
@@ -8,7 +8,6 @@ from fastapi.responses import Response
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.app.core.auth import RequireAdminIfAuthEnabled
|
||||
from backend.app.core.config import settings
|
||||
from backend.app.core.database import get_db
|
||||
from backend.app.models.printer import Printer
|
||||
@@ -48,7 +47,6 @@ async def list_printers(db: AsyncSession = Depends(get_db)):
|
||||
async def create_printer(
|
||||
printer_data: PrinterCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_current_user=RequireAdminIfAuthEnabled(),
|
||||
):
|
||||
"""Add a new printer."""
|
||||
# Check if serial number already exists
|
||||
@@ -83,7 +81,6 @@ async def update_printer(
|
||||
printer_id: int,
|
||||
printer_data: PrinterUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_current_user=RequireAdminIfAuthEnabled(),
|
||||
):
|
||||
"""Update a printer."""
|
||||
result = await db.execute(select(Printer).where(Printer.id == printer_id))
|
||||
@@ -112,7 +109,6 @@ async def delete_printer(
|
||||
printer_id: int,
|
||||
delete_archives: bool = True,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_current_user=RequireAdminIfAuthEnabled(),
|
||||
):
|
||||
"""Delete a printer.
|
||||
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.app.core.auth import RequireAdmin, get_password_hash
|
||||
from backend.app.core.database import get_db
|
||||
from backend.app.models.user import User
|
||||
from backend.app.schemas.auth import UserCreate, UserResponse, UserUpdate
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["users"])
|
||||
|
||||
|
||||
@router.get("", response_model=list[UserResponse])
|
||||
@router.get("/", response_model=list[UserResponse])
|
||||
async def list_users(
|
||||
current_user: User = RequireAdmin(),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""List all users (admin only)."""
|
||||
result = await db.execute(select(User).order_by(User.created_at))
|
||||
users = result.scalars().all()
|
||||
return [
|
||||
UserResponse(
|
||||
id=user.id,
|
||||
username=user.username,
|
||||
role=user.role,
|
||||
is_active=user.is_active,
|
||||
created_at=user.created_at.isoformat(),
|
||||
)
|
||||
for user in users
|
||||
]
|
||||
|
||||
|
||||
@router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
||||
@router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_user(
|
||||
user_data: UserCreate,
|
||||
current_user: User = RequireAdmin(),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Create a new user (admin only)."""
|
||||
# Check if username already exists
|
||||
existing_user = await db.execute(select(User).where(User.username == user_data.username))
|
||||
if existing_user.scalar_one_or_none():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Username already exists",
|
||||
)
|
||||
|
||||
# Validate role
|
||||
if user_data.role not in ["admin", "user"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Role must be 'admin' or 'user'",
|
||||
)
|
||||
|
||||
new_user = User(
|
||||
username=user_data.username,
|
||||
password_hash=get_password_hash(user_data.password),
|
||||
role=user_data.role,
|
||||
is_active=True,
|
||||
)
|
||||
db.add(new_user)
|
||||
await db.commit()
|
||||
await db.refresh(new_user)
|
||||
|
||||
return UserResponse(
|
||||
id=new_user.id,
|
||||
username=new_user.username,
|
||||
role=new_user.role,
|
||||
is_active=new_user.is_active,
|
||||
created_at=new_user.created_at.isoformat(),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=UserResponse)
|
||||
async def get_user(
|
||||
user_id: int,
|
||||
current_user: User = RequireAdmin(),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Get a user by ID (admin only)."""
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found",
|
||||
)
|
||||
|
||||
return UserResponse(
|
||||
id=user.id,
|
||||
username=user.username,
|
||||
role=user.role,
|
||||
is_active=user.is_active,
|
||||
created_at=user.created_at.isoformat(),
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/{user_id}", response_model=UserResponse)
|
||||
async def update_user(
|
||||
user_id: int,
|
||||
user_data: UserUpdate,
|
||||
current_user: User = RequireAdmin(),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Update a user (admin only)."""
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found",
|
||||
)
|
||||
|
||||
# Prevent deactivating the last admin
|
||||
if user_data.is_active is False and user.role == "admin":
|
||||
admin_count_result = await db.execute(select(User).where(User.role == "admin", User.is_active.is_(True)))
|
||||
admin_count = len(admin_count_result.scalars().all())
|
||||
if admin_count <= 1:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Cannot deactivate the last admin user",
|
||||
)
|
||||
|
||||
# Prevent changing role of last admin
|
||||
if user_data.role and user_data.role != "admin" and user.role == "admin":
|
||||
admin_count_result = await db.execute(select(User).where(User.role == "admin", User.is_active.is_(True)))
|
||||
admin_count = len(admin_count_result.scalars().all())
|
||||
if admin_count <= 1:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Cannot change role of the last admin user",
|
||||
)
|
||||
|
||||
if user_data.username is not None:
|
||||
# Check if new username already exists
|
||||
existing_user = await db.execute(select(User).where(User.username == user_data.username, User.id != user_id))
|
||||
if existing_user.scalar_one_or_none():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Username already exists",
|
||||
)
|
||||
user.username = user_data.username
|
||||
|
||||
if user_data.password is not None:
|
||||
user.password_hash = get_password_hash(user_data.password)
|
||||
|
||||
if user_data.role is not None:
|
||||
if user_data.role not in ["admin", "user"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Role must be 'admin' or 'user'",
|
||||
)
|
||||
user.role = user_data.role
|
||||
|
||||
if user_data.is_active is not None:
|
||||
user.is_active = user_data.is_active
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
|
||||
return UserResponse(
|
||||
id=user.id,
|
||||
username=user.username,
|
||||
role=user.role,
|
||||
is_active=user.is_active,
|
||||
created_at=user.created_at.isoformat(),
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_user(
|
||||
user_id: int,
|
||||
current_user: User = RequireAdmin(),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Delete a user (admin only)."""
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found",
|
||||
)
|
||||
|
||||
# Prevent deleting the last admin
|
||||
if user.role == "admin":
|
||||
admin_count_result = await db.execute(select(User).where(User.role == "admin", User.id != user_id))
|
||||
admin_count = len(admin_count_result.scalars().all())
|
||||
if admin_count == 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Cannot delete the last admin user",
|
||||
)
|
||||
|
||||
# Prevent deleting yourself
|
||||
if user.id == current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Cannot delete your own account",
|
||||
)
|
||||
|
||||
await db.delete(user)
|
||||
await db.commit()
|
||||
Reference in New Issue
Block a user