Browse Source

Revert "Added optional authentication and user management"

MartinNYHC 4 months ago
parent
commit
a9f340f2c9

+ 0 - 213
backend/app/api/routes/auth.py

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

+ 0 - 4
backend/app/api/routes/printers.py

@@ -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.
 

+ 0 - 205
backend/app/api/routes/users.py

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

+ 61 - 306
backend/app/core/auth.py

@@ -1,350 +1,105 @@
+import hashlib
 import secrets
-from datetime import datetime, timedelta
-from typing import TYPE_CHECKING, Annotated
+from datetime import datetime
 
-from fastapi import Depends, Header, HTTPException, status
-from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
-from jose import JWTError, jwt
-from passlib.context import CryptContext
+from fastapi import Depends, Header, HTTPException
 from sqlalchemy import select
 from sqlalchemy.ext.asyncio import AsyncSession
 
-from backend.app.core.database import async_session, get_db
-from backend.app.models.settings import Settings
-from backend.app.models.user import User
+from backend.app.core.database import get_db
+from backend.app.models.api_key import APIKey
 
-if TYPE_CHECKING:
-    from backend.app.models.api_key import APIKey
 
-# Password hashing
-# Use pbkdf2_sha256 instead of bcrypt to avoid 72-byte limit and passlib initialization issues
-# pbkdf2_sha256 is a secure password hashing algorithm without bcrypt's limitations
-pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
-
-# JWT settings
-SECRET_KEY = "bambuddy-secret-key-change-in-production"  # TODO: Move to settings/env
-ALGORITHM = "HS256"
-ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7  # 7 days
-
-# HTTP Bearer token
-security = HTTPBearer(auto_error=False)
-
-
-def verify_password(plain_password: str, hashed_password: str) -> bool:
-    """Verify a password against a hash.
+def generate_api_key() -> tuple[str, str, str]:
+    """Generate a new API key.
 
-    Uses pbkdf2_sha256 which handles long passwords automatically.
+    Returns:
+        Tuple of (full_key, key_hash, key_prefix)
     """
-    return pwd_context.verify(plain_password, hashed_password)
+    # Generate a random 32-byte key and encode as hex (64 chars)
+    full_key = f"bb_{secrets.token_hex(32)}"
+    key_hash = hashlib.sha256(full_key.encode()).hexdigest()
+    key_prefix = full_key[:11]  # "bb_" + first 8 chars of token
+    return full_key, key_hash, key_prefix
 
 
-def get_password_hash(password: str) -> str:
-    """Hash a password.
+def hash_api_key(key: str) -> str:
+    """Hash an API key for comparison."""
+    return hashlib.sha256(key.encode()).hexdigest()
 
-    Uses pbkdf2_sha256 which is secure and has no password length limit.
-    """
-    return pwd_context.hash(password)
 
+async def get_api_key(
+    x_api_key: str = Header(..., alias="X-API-Key"),
+    db: AsyncSession = Depends(get_db),
+) -> APIKey:
+    """Verify API key and return the key record.
 
-def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
-    """Create a JWT access token."""
-    to_encode = data.copy()
-    if expires_delta:
-        expire = datetime.utcnow() + expires_delta
-    else:
-        expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
-    to_encode.update({"exp": expire})
-    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
-    return encoded_jwt
+    Raises HTTPException if key is invalid, disabled, or expired.
+    """
+    key_hash = hash_api_key(x_api_key)
 
+    result = await db.execute(select(APIKey).where(APIKey.key_hash == key_hash))
+    api_key = result.scalar_one_or_none()
 
-async def get_user_by_username(db: AsyncSession, username: str) -> User | None:
-    """Get a user by username."""
-    result = await db.execute(select(User).where(User.username == username))
-    return result.scalar_one_or_none()
+    if not api_key:
+        raise HTTPException(status_code=401, detail="Invalid API key")
 
+    if not api_key.enabled:
+        raise HTTPException(status_code=403, detail="API key is disabled")
 
-async def authenticate_user(db: AsyncSession, username: str, password: str) -> User | None:
-    """Authenticate a user by username and password."""
-    user = await get_user_by_username(db, username)
-    if not user:
-        return None
-    if not verify_password(password, user.password_hash):
-        return None
-    if not user.is_active:
-        return None
-    return user
+    if api_key.expires_at and api_key.expires_at < datetime.utcnow():
+        raise HTTPException(status_code=403, detail="API key has expired")
 
+    # Update last_used timestamp
+    api_key.last_used = datetime.utcnow()
 
-async def is_auth_enabled(db: AsyncSession) -> bool:
-    """Check if authentication is enabled."""
-    try:
-        result = await db.execute(select(Settings).where(Settings.key == "auth_enabled"))
-        setting = result.scalar_one_or_none()
-        return setting and setting.value.lower() == "true"
-    except Exception:
-        # If settings table doesn't exist or query fails, assume auth is disabled
-        return False
+    return api_key
 
 
-async def get_current_user_optional(
-    credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(security)] = None,
-) -> User | None:
-    """Get the current authenticated user from JWT token, or None if not authenticated."""
-    if credentials is None:
+async def get_optional_api_key(
+    x_api_key: str | None = Header(None, alias="X-API-Key"),
+    db: AsyncSession = Depends(get_db),
+) -> APIKey | None:
+    """Get API key if provided, return None otherwise."""
+    if not x_api_key:
         return None
 
     try:
-        token = credentials.credentials
-        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
-        username: str = payload.get("sub")
-        if username is None:
-            return None
-    except JWTError:
+        return await get_api_key(x_api_key, db)
+    except HTTPException:
         return None
 
-    async with async_session() as db:
-        user = await get_user_by_username(db, username)
-        if user is None or not user.is_active:
-            return None
-        return user
-
-
-async def get_current_user(credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)]) -> User:
-    """Get the current authenticated user from JWT token."""
-    credentials_exception = HTTPException(
-        status_code=status.HTTP_401_UNAUTHORIZED,
-        detail="Could not validate credentials",
-        headers={"WWW-Authenticate": "Bearer"},
-    )
-    try:
-        token = credentials.credentials
-        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
-        username: str = payload.get("sub")
-        if username is None:
-            raise credentials_exception
-    except JWTError:
-        raise credentials_exception
-
-    async with async_session() as db:
-        user = await get_user_by_username(db, username)
-        if user is None:
-            raise credentials_exception
-        if not user.is_active:
-            raise HTTPException(
-                status_code=status.HTTP_403_FORBIDDEN,
-                detail="User account is disabled",
-            )
-        return user
-
-
-async def get_current_active_user(current_user: Annotated[User, Depends(get_current_user)]) -> User:
-    """Get the current active user (alias for clarity)."""
-    return current_user
-
-
-async def require_auth_if_enabled(
-    credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(security)] = None,
-) -> User | None:
-    """Require authentication if auth is enabled, otherwise return None."""
-    async with async_session() as db:
-        auth_enabled = await is_auth_enabled(db)
-        if not auth_enabled:
-            return None
-
-        if credentials is None:
-            raise HTTPException(
-                status_code=status.HTTP_401_UNAUTHORIZED,
-                detail="Authentication required",
-                headers={"WWW-Authenticate": "Bearer"},
-            )
-
-        try:
-            token = credentials.credentials
-            payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
-            username: str = payload.get("sub")
-            if username is None:
-                raise HTTPException(
-                    status_code=status.HTTP_401_UNAUTHORIZED,
-                    detail="Could not validate credentials",
-                    headers={"WWW-Authenticate": "Bearer"},
-                )
-        except JWTError:
-            raise HTTPException(
-                status_code=status.HTTP_401_UNAUTHORIZED,
-                detail="Could not validate credentials",
-                headers={"WWW-Authenticate": "Bearer"},
-            )
-
-        user = await get_user_by_username(db, username)
-        if user is None or not user.is_active:
-            raise HTTPException(
-                status_code=status.HTTP_401_UNAUTHORIZED,
-                detail="Could not validate credentials",
-                headers={"WWW-Authenticate": "Bearer"},
-            )
-        return user
-
-
-def require_role(required_role: str):
-    """Dependency factory for role-based access control."""
-
-    async def role_checker(current_user: Annotated[User, Depends(get_current_user)]) -> User:
-        if current_user.role != required_role:
-            raise HTTPException(
-                status_code=status.HTTP_403_FORBIDDEN,
-                detail=f"Requires {required_role} role",
-            )
-        return current_user
-
-    return role_checker
 
-
-def require_admin_if_auth_enabled():
-    """Dependency factory that requires admin role if auth is enabled."""
-
-    async def admin_checker(
-        current_user: Annotated[User | None, Depends(require_auth_if_enabled)] = None,
-    ) -> User | None:
-        if current_user is None:
-            return None  # Auth not enabled, allow access
-        if current_user.role != "admin":
-            raise HTTPException(
-                status_code=status.HTTP_403_FORBIDDEN,
-                detail="Requires admin role",
-            )
-        return current_user
-
-    return admin_checker
-
-
-def generate_api_key() -> tuple[str, str, str]:
-    """Generate a new API key.
-
-    Returns:
-        tuple: (full_key, key_hash, key_prefix)
-            - full_key: The complete API key (only shown once on creation)
-            - key_hash: Hashed version for storage and verification
-            - key_prefix: First 8 characters for display purposes
-    """
-    # Generate a secure random API key (32 bytes = 64 hex characters)
-    full_key = f"bb_{secrets.token_urlsafe(32)}"
-    key_hash = get_password_hash(full_key)
-    key_prefix = full_key[:8] + "..." if len(full_key) > 8 else full_key
-    return full_key, key_hash, key_prefix
-
-
-async def get_api_key(
-    authorization: Annotated[str | None, Header(alias="Authorization")] = None,
-    x_api_key: Annotated[str | None, Header(alias="X-API-Key")] = None,
-    db: AsyncSession = Depends(get_db),
-) -> "APIKey":
-    """Get and validate API key from request headers.
-
-    Checks both 'Authorization: Bearer <key>' and 'X-API-Key: <key>' headers.
-    """
-    from fastapi import HTTPException, status
-
-    from backend.app.models.api_key import APIKey
-
-    api_key_value = None
-    if x_api_key:
-        api_key_value = x_api_key
-    elif authorization and authorization.startswith("Bearer "):
-        api_key_value = authorization.replace("Bearer ", "")
-
-    if not api_key_value:
-        raise HTTPException(
-            status_code=status.HTTP_401_UNAUTHORIZED,
-            detail="API key required. Provide 'X-API-Key' header or 'Authorization: Bearer <key>'",
-        )
-
-    # Get all API keys and check them
-    result = await db.execute(select(APIKey).where(APIKey.enabled.is_(True)))
-    api_keys = result.scalars().all()
-
-    for api_key in api_keys:
-        # Check if key matches (verify against hash)
-        if verify_password(api_key_value, api_key.key_hash):
-            # Check expiration
-            if api_key.expires_at and api_key.expires_at < datetime.now():
-                raise HTTPException(
-                    status_code=status.HTTP_401_UNAUTHORIZED,
-                    detail="API key has expired",
-                )
-            # Update last_used timestamp
-            api_key.last_used = datetime.now()
-            await db.commit()
-            return api_key
-
-    raise HTTPException(
-        status_code=status.HTTP_401_UNAUTHORIZED,
-        detail="Invalid API key",
-    )
-
-
-def check_permission(api_key: "APIKey", permission: str) -> None:
-    """Check if API key has the required permission.
+def check_permission(api_key: APIKey, permission: str) -> None:
+    """Check if API key has a specific permission.
 
     Args:
-        api_key: The API key object
+        api_key: The API key record
         permission: One of 'queue', 'control_printer', 'read_status'
 
-    Raises:
-        HTTPException: If permission is not granted
+    Raises HTTPException if permission is denied.
     """
-    from fastapi import HTTPException, status
-
     permission_map = {
-        "queue": "can_queue",
-        "control_printer": "can_control_printer",
-        "read_status": "can_read_status",
+        "queue": api_key.can_queue,
+        "control_printer": api_key.can_control_printer,
+        "read_status": api_key.can_read_status,
     }
 
     if permission not in permission_map:
-        raise HTTPException(
-            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
-            detail=f"Unknown permission: {permission}",
-        )
+        raise HTTPException(status_code=500, detail=f"Unknown permission: {permission}")
 
-    attr_name = permission_map[permission]
-    if not getattr(api_key, attr_name, False):
-        raise HTTPException(
-            status_code=status.HTTP_403_FORBIDDEN,
-            detail=f"API key does not have '{permission}' permission",
-        )
+    if not permission_map[permission]:
+        raise HTTPException(status_code=403, detail=f"API key does not have '{permission}' permission")
 
 
-def check_printer_access(api_key: "APIKey", printer_id: int) -> None:
-    """Check if API key has access to the specified printer.
+def check_printer_access(api_key: APIKey, printer_id: int) -> None:
+    """Check if API key has access to a specific printer.
 
     Args:
-        api_key: The API key object
-        printer_id: The printer ID to check access for
+        api_key: The API key record
+        printer_id: The printer ID to check
 
-    Raises:
-        HTTPException: If access is denied
+    Raises HTTPException if access is denied.
     """
-    from fastapi import HTTPException, status
-
-    # If printer_ids is None or empty, access to all printers
-    if api_key.printer_ids is None or len(api_key.printer_ids) == 0:
-        return
-
-    # Check if printer_id is in allowed list
-    if printer_id not in api_key.printer_ids:
-        raise HTTPException(
-            status_code=status.HTTP_403_FORBIDDEN,
-            detail=f"API key does not have access to printer {printer_id}",
-        )
-
-
-# Convenience dependencies - these are functions that return Depends objects
-def RequireAdmin():
-    """Dependency that requires admin role."""
-    return Depends(require_role("admin"))
-
-
-def RequireAdminIfAuthEnabled():
-    """Dependency that requires admin role if auth is enabled."""
-    return Depends(require_admin_if_auth_enabled())
+    if api_key.printer_ids is not None and printer_id not in api_key.printer_ids:
+        raise HTTPException(status_code=403, detail=f"API key does not have access to printer {printer_id}")

+ 0 - 20
backend/app/core/database.py

@@ -49,7 +49,6 @@ async def init_db():
         project_bom,
         settings,
         smart_plug,
-        user,
     )
 
     async with engine.begin() as conn:
@@ -642,25 +641,6 @@ async def run_migrations(conn):
     except Exception:
         pass
 
-    # Migration: Create users table for authentication
-    try:
-        await conn.execute(
-            text("""
-            CREATE TABLE IF NOT EXISTS users (
-                id INTEGER PRIMARY KEY,
-                username VARCHAR(100) NOT NULL UNIQUE,
-                password_hash VARCHAR(255) NOT NULL,
-                role VARCHAR(20) NOT NULL DEFAULT 'user',
-                is_active BOOLEAN NOT NULL DEFAULT 1,
-                created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
-                updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
-            )
-        """)
-        )
-        await conn.execute(text("CREATE INDEX IF NOT EXISTS ix_users_username ON users(username)"))
-    except Exception:
-        pass
-
 
 async def seed_notification_templates():
     """Seed default notification templates if they don't exist."""

+ 0 - 4
backend/app/main.py

@@ -54,7 +54,6 @@ from backend.app.api.routes import (
     ams_history,
     api_keys,
     archives,
-    auth,
     camera,
     cloud,
     discovery,
@@ -76,7 +75,6 @@ from backend.app.api.routes import (
     support,
     system,
     updates,
-    users,
     webhook,
     websocket,
 )
@@ -1963,8 +1961,6 @@ app = FastAPI(
 )
 
 # API routes
-app.include_router(auth.router, prefix=app_settings.api_prefix)
-app.include_router(users.router, prefix=app_settings.api_prefix)
 app.include_router(printers.router, prefix=app_settings.api_prefix)
 app.include_router(archives.router, prefix=app_settings.api_prefix)
 app.include_router(filaments.router, prefix=app_settings.api_prefix)

+ 0 - 2
backend/app/models/__init__.py

@@ -12,7 +12,6 @@ from backend.app.models.printer import Printer
 from backend.app.models.project import Project
 from backend.app.models.settings import Settings
 from backend.app.models.smart_plug import SmartPlug
-from backend.app.models.user import User
 
 __all__ = [
     "Printer",
@@ -32,5 +31,4 @@ __all__ = [
     "PendingUpload",
     "LibraryFolder",
     "LibraryFile",
-    "User",
 ]

+ 0 - 20
backend/app/models/user.py

@@ -1,20 +0,0 @@
-from datetime import datetime
-
-from sqlalchemy import DateTime, String, func
-from sqlalchemy.orm import Mapped, mapped_column
-
-from backend.app.core.database import Base
-
-
-class User(Base):
-    """User model for authentication and authorization."""
-
-    __tablename__ = "users"
-
-    id: Mapped[int] = mapped_column(primary_key=True)
-    username: Mapped[str] = mapped_column(String(100), unique=True, index=True)
-    password_hash: Mapped[str] = mapped_column(String(255))
-    role: Mapped[str] = mapped_column(String(20), default="user")  # "admin" or "user"
-    is_active: Mapped[bool] = mapped_column(default=True)
-    created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
-    updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now())

+ 0 - 47
backend/app/schemas/auth.py

@@ -1,47 +0,0 @@
-from pydantic import BaseModel
-
-
-class LoginRequest(BaseModel):
-    username: str
-    password: str
-
-
-class LoginResponse(BaseModel):
-    access_token: str
-    token_type: str = "bearer"
-    user: "UserResponse"
-
-
-class UserCreate(BaseModel):
-    username: str
-    password: str
-    role: str = "user"
-
-
-class UserUpdate(BaseModel):
-    username: str | None = None
-    password: str | None = None
-    role: str | None = None
-    is_active: bool | None = None
-
-
-class UserResponse(BaseModel):
-    id: int
-    username: str
-    role: str
-    is_active: bool
-    created_at: str
-
-    class Config:
-        from_attributes = True
-
-
-class SetupRequest(BaseModel):
-    auth_enabled: bool
-    admin_username: str | None = None
-    admin_password: str | None = None
-
-
-class SetupResponse(BaseModel):
-    auth_enabled: bool
-    admin_created: bool | None = None

+ 0 - 1
backend/tests/conftest.py

@@ -59,7 +59,6 @@ async def test_engine():
         project,
         settings,
         smart_plug,
-        user,
     )
 
     async with engine.begin() as conn:

+ 14 - 39
frontend/package-lock.json

@@ -11,7 +11,6 @@
         "@dnd-kit/core": "^6.3.1",
         "@dnd-kit/sortable": "^10.0.0",
         "@dnd-kit/utilities": "^3.2.2",
-        "@floating-ui/dom": "^1.7.4",
         "@tanstack/react-query": "^5.90.11",
         "@tiptap/extension-color": "^3.11.1",
         "@tiptap/extension-image": "^3.11.1",
@@ -143,7 +142,6 @@
       "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@babel/code-frame": "^7.27.1",
         "@babel/generator": "^7.28.5",
@@ -494,7 +492,6 @@
           "url": "https://opencollective.com/csstools"
         }
       ],
-      "peer": true,
       "engines": {
         "node": ">=18"
       },
@@ -517,7 +514,6 @@
           "url": "https://opencollective.com/csstools"
         }
       ],
-      "peer": true,
       "engines": {
         "node": ">=18"
       }
@@ -545,7 +541,6 @@
       "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
       "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@dnd-kit/accessibility": "^3.1.1",
         "@dnd-kit/utilities": "^3.2.2",
@@ -1186,6 +1181,7 @@
       "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
       "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
       "license": "MIT",
+      "optional": true,
       "dependencies": {
         "@floating-ui/utils": "^0.2.10"
       }
@@ -1194,8 +1190,7 @@
       "version": "1.7.4",
       "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
       "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
-      "license": "MIT",
-      "peer": true,
+      "optional": true,
       "dependencies": {
         "@floating-ui/core": "^1.7.3",
         "@floating-ui/utils": "^0.2.10"
@@ -1205,7 +1200,8 @@
       "version": "0.2.10",
       "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
       "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
-      "license": "MIT"
+      "license": "MIT",
+      "optional": true
     },
     "node_modules/@humanfs/core": {
       "version": "0.19.1",
@@ -2312,7 +2308,6 @@
       "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.11.1.tgz",
       "integrity": "sha512-q7uzYrCq40JOIi6lceWe2HuA8tSr97iPwP/xtJd0bZjyL1rWhUyqxMb7y+aq4RcELrx/aNRa2JIvLtRRdy02Dg==",
       "license": "MIT",
-      "peer": true,
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -2561,7 +2556,6 @@
       "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.11.1.tgz",
       "integrity": "sha512-XJRN9pOPMi3SsaKv4qM8WBEi3YDrjXYtYlAlZutQe1JpdKykSjLwwYq7k3V8UHqR3YKxyOV8HTYOYoOaZ9TMTQ==",
       "license": "MIT",
-      "peer": true,
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -2667,7 +2661,6 @@
       "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.11.1.tgz",
       "integrity": "sha512-KLLrABvf609/Z4dPChRowvpqeefYiq5csEj4Ogfp4EFd3KqDvPZIoFepau1+BW4gOAlm8UK+ig+fOLgnUzH7ww==",
       "license": "MIT",
-      "peer": true,
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -2694,7 +2687,6 @@
       "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.11.1.tgz",
       "integrity": "sha512-/xXJdV+EVvSQv2slvAUChb5iGVv5K0EqBqxPGAAuBHdIc4Y7Id1aaKKSiyDmqon+kjSnnQIIda9oUt+o/Z66uA==",
       "license": "MIT",
-      "peer": true,
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -2709,7 +2701,6 @@
       "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.11.1.tgz",
       "integrity": "sha512-8RIUhlEoCFGsbdNb+EUdQctG1Wnd7rl4wlMLS6giO7UcZT5dVfg625eMZVrl0/kA7JBJdKLIuqNmzzQ0MxsJEw==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "prosemirror-changeset": "^2.3.0",
         "prosemirror-collab": "^1.3.1",
@@ -2808,7 +2799,8 @@
       "version": "5.0.4",
       "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
       "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
-      "dev": true
+      "dev": true,
+      "peer": true
     },
     "node_modules/@types/babel__core": {
       "version": "7.20.5",
@@ -2960,7 +2952,6 @@
       "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "undici-types": "~7.16.0"
       }
@@ -2970,7 +2961,6 @@
       "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
       "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "csstype": "^3.2.2"
       }
@@ -2980,7 +2970,6 @@
       "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
       "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
       "license": "MIT",
-      "peer": true,
       "peerDependencies": {
         "@types/react": "^19.2.0"
       }
@@ -3070,7 +3059,6 @@
       "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@typescript-eslint/scope-manager": "8.48.0",
         "@typescript-eslint/types": "8.48.0",
@@ -3440,7 +3428,6 @@
       "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "bin": {
         "acorn": "bin/acorn"
       },
@@ -3625,7 +3612,6 @@
         }
       ],
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "baseline-browser-mapping": "^2.8.25",
         "caniuse-lite": "^1.0.30001754",
@@ -4188,7 +4174,8 @@
       "version": "0.5.16",
       "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
       "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
-      "dev": true
+      "dev": true,
+      "peer": true
     },
     "node_modules/dunder-proto": {
       "version": "1.0.1",
@@ -4380,7 +4367,6 @@
       "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.8.0",
         "@eslint-community/regexpp": "^4.12.1",
@@ -5068,7 +5054,6 @@
         }
       ],
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@babel/runtime": "^7.28.4"
       },
@@ -5841,6 +5826,7 @@
       "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
       "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
       "dev": true,
+      "peer": true,
       "bin": {
         "lz-string": "bin/bin.js"
       }
@@ -6329,7 +6315,6 @@
       "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "engines": {
         "node": ">=12"
       },
@@ -6357,7 +6342,6 @@
         }
       ],
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "nanoid": "^3.3.11",
         "picocolors": "^1.1.1",
@@ -6389,6 +6373,7 @@
       "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
       "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
       "dev": true,
+      "peer": true,
       "dependencies": {
         "ansi-regex": "^5.0.1",
         "ansi-styles": "^5.0.0",
@@ -6403,6 +6388,7 @@
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
       "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
       "dev": true,
+      "peer": true,
       "engines": {
         "node": ">=10"
       },
@@ -6414,7 +6400,8 @@
       "version": "17.0.2",
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
       "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
-      "dev": true
+      "dev": true,
+      "peer": true
     },
     "node_modules/process-nextick-args": {
       "version": "2.0.1",
@@ -6534,7 +6521,6 @@
       "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
       "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "orderedmap": "^2.0.0"
       }
@@ -6564,7 +6550,6 @@
       "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
       "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "prosemirror-model": "^1.0.0",
         "prosemirror-transform": "^1.0.0",
@@ -6613,7 +6598,6 @@
       "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.3.tgz",
       "integrity": "sha512-SqMiYMUQNNBP9kfPhLO8WXEk/fon47vc52FQsUiJzTBuyjKgEcoAwMyF04eQ4WZ2ArMn7+ReypYL60aKngbACQ==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "prosemirror-model": "^1.20.0",
         "prosemirror-state": "^1.0.0",
@@ -6644,7 +6628,6 @@
       "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
       "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
       "license": "MIT",
-      "peer": true,
       "engines": {
         "node": ">=0.10.0"
       }
@@ -6654,7 +6637,6 @@
       "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
       "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "scheduler": "^0.27.0"
       },
@@ -6701,7 +6683,6 @@
       "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
       "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@types/use-sync-external-store": "^0.0.6",
         "use-sync-external-store": "^1.4.0"
@@ -6830,8 +6811,7 @@
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
       "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
-      "license": "MIT",
-      "peer": true
+      "license": "MIT"
     },
     "node_modules/redux-thunk": {
       "version": "3.1.0",
@@ -7428,7 +7408,6 @@
       "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
       "devOptional": true,
       "license": "Apache-2.0",
-      "peer": true,
       "bin": {
         "tsc": "bin/tsc",
         "tsserver": "bin/tsserver"
@@ -7567,7 +7546,6 @@
       "integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "esbuild": "^0.25.0",
         "fdir": "^6.5.0",
@@ -8129,7 +8107,6 @@
       "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz",
       "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==",
       "dev": true,
-      "peer": true,
       "dependencies": {
         "@vitest/expect": "2.1.9",
         "@vitest/mocker": "2.1.9",
@@ -8627,7 +8604,6 @@
       "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
       "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
       "dev": true,
-      "peer": true,
       "dependencies": {
         "esbuild": "^0.21.3",
         "postcss": "^8.4.43",
@@ -9026,7 +9002,6 @@
       "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "funding": {
         "url": "https://github.com/sponsors/colinhacks"
       }

+ 0 - 1
frontend/package.json

@@ -17,7 +17,6 @@
     "@dnd-kit/core": "^6.3.1",
     "@dnd-kit/sortable": "^10.0.0",
     "@dnd-kit/utilities": "^3.2.2",
-    "@floating-ui/dom": "^1.7.4",
     "@tanstack/react-query": "^5.90.11",
     "@tiptap/extension-color": "^3.11.1",
     "@tiptap/extension-image": "^3.11.1",

+ 23 - 89
frontend/src/App.tsx

@@ -1,4 +1,4 @@
-import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
+import { BrowserRouter, Routes, Route } from 'react-router-dom';
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
 import { Layout } from './components/Layout';
 import { PrintersPage } from './pages/PrintersPage';
@@ -14,13 +14,9 @@ import { FileManagerPage } from './pages/FileManagerPage';
 import { CameraPage } from './pages/CameraPage';
 import { ExternalLinkPage } from './pages/ExternalLinkPage';
 import { SystemInfoPage } from './pages/SystemInfoPage';
-import { LoginPage } from './pages/LoginPage';
-import { SetupPage } from './pages/SetupPage';
-import { UsersPage } from './pages/UsersPage';
 import { useWebSocket } from './hooks/useWebSocket';
 import { ThemeProvider } from './contexts/ThemeContext';
 import { ToastProvider } from './contexts/ToastContext';
-import { AuthProvider, useAuth } from './contexts/AuthContext';
 
 const queryClient = new QueryClient({
   defaultOptions: {
@@ -36,95 +32,33 @@ function WebSocketProvider({ children }: { children: React.ReactNode }) {
   return <>{children}</>;
 }
 
-function ProtectedRoute({ children }: { children: React.ReactNode }) {
-  const { authEnabled, loading, user } = useAuth();
-
-  if (loading) {
-    return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
-  }
-
-  if (authEnabled && !user) {
-    return <Navigate to="/login" replace />;
-  }
-
-  return <>{children}</>;
-}
-
-function AdminRoute({ children }: { children: React.ReactNode }) {
-  const { authEnabled, loading, user } = useAuth();
-
-  if (loading) {
-    return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
-  }
-
-  // If auth is not enabled, allow access (backward compatibility)
-  if (!authEnabled) {
-    return <>{children}</>;
-  }
-
-  // If auth is enabled but no user, redirect to login
-  if (!user) {
-    return <Navigate to="/login" replace />;
-  }
-
-  // If user is not admin, redirect to home
-  if (user.role !== 'admin') {
-    return <Navigate to="/" replace />;
-  }
-
-  return <>{children}</>;
-}
-
-function SetupRoute({ children }: { children: React.ReactNode }) {
-  const { authEnabled, loading } = useAuth();
-
-  if (loading) {
-    return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
-  }
-
-  if (authEnabled) {
-    return <Navigate to="/login" replace />;
-  }
-
-  return <>{children}</>;
-}
-
 function App() {
   return (
     <ThemeProvider>
       <ToastProvider>
         <QueryClientProvider client={queryClient}>
-          <AuthProvider>
-            <BrowserRouter>
-              <Routes>
-                {/* Setup page - only accessible if auth not enabled */}
-                <Route path="/setup" element={<SetupRoute><SetupPage /></SetupRoute>} />
-
-                {/* Login page */}
-                <Route path="/login" element={<LoginPage />} />
-
-                {/* Camera page - standalone, no layout, no WebSocket (doesn't need real-time updates) */}
-                <Route path="/camera/:printerId" element={<CameraPage />} />
-
-                {/* Main app with WebSocket for real-time updates */}
-                <Route element={<ProtectedRoute><WebSocketProvider><Layout /></WebSocketProvider></ProtectedRoute>}>
-                  <Route index element={<PrintersPage />} />
-                  <Route path="archives" element={<ArchivesPage />} />
-                  <Route path="queue" element={<QueuePage />} />
-                  <Route path="stats" element={<StatsPage />} />
-                  <Route path="profiles" element={<ProfilesPage />} />
-                  <Route path="maintenance" element={<MaintenancePage />} />
-                  <Route path="projects" element={<ProjectsPage />} />
-                  <Route path="projects/:id" element={<ProjectDetailPage />} />
-                  <Route path="files" element={<FileManagerPage />} />
-                  <Route path="settings" element={<AdminRoute><SettingsPage /></AdminRoute>} />
-                  <Route path="users" element={<UsersPage />} />
-                  <Route path="system" element={<SystemInfoPage />} />
-                  <Route path="external/:id" element={<ExternalLinkPage />} />
-                </Route>
-              </Routes>
-            </BrowserRouter>
-          </AuthProvider>
+          <BrowserRouter>
+            <Routes>
+              {/* Camera page - standalone, no layout, no WebSocket (doesn't need real-time updates) */}
+              <Route path="/camera/:printerId" element={<CameraPage />} />
+
+              {/* Main app with WebSocket for real-time updates */}
+              <Route element={<WebSocketProvider><Layout /></WebSocketProvider>}>
+                <Route index element={<PrintersPage />} />
+                <Route path="archives" element={<ArchivesPage />} />
+                <Route path="queue" element={<QueuePage />} />
+                <Route path="stats" element={<StatsPage />} />
+                <Route path="profiles" element={<ProfilesPage />} />
+                <Route path="maintenance" element={<MaintenancePage />} />
+                <Route path="projects" element={<ProjectsPage />} />
+                <Route path="projects/:id" element={<ProjectDetailPage />} />
+                <Route path="files" element={<FileManagerPage />} />
+                <Route path="settings" element={<SettingsPage />} />
+                <Route path="system" element={<SystemInfoPage />} />
+                <Route path="external/:id" element={<ExternalLinkPage />} />
+              </Route>
+            </Routes>
+          </BrowserRouter>
         </QueryClientProvider>
       </ToastProvider>
     </ThemeProvider>

+ 0 - 3
frontend/src/__tests__/components/Layout.test.tsx

@@ -49,9 +49,6 @@ describe('Layout', () => {
       }),
       http.get('/api/v1/updates/check', () => {
         return HttpResponse.json({ update_available: false });
-      }),
-      http.get('/api/v1/auth/status', () => {
-        return HttpResponse.json({ auth_enabled: false, requires_setup: false });
       })
     );
   });

+ 0 - 3
frontend/src/__tests__/pages/SettingsPage.test.tsx

@@ -60,9 +60,6 @@ describe('SettingsPage', () => {
       }),
       http.get('/api/v1/virtual-printer/status', () => {
         return HttpResponse.json({ running: false });
-      }),
-      http.get('/api/v1/auth/status', () => {
-        return HttpResponse.json({ auth_enabled: false, requires_setup: false });
       })
     );
   });

+ 1 - 4
frontend/src/__tests__/utils.tsx

@@ -9,7 +9,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
 import { BrowserRouter } from 'react-router-dom';
 import { ThemeProvider } from '../contexts/ThemeContext';
 import { ToastProvider } from '../contexts/ToastContext';
-import { AuthProvider } from '../contexts/AuthContext';
 
 // Create a new QueryClient for each test
 function createTestQueryClient() {
@@ -37,9 +36,7 @@ function AllProviders({ children }: AllProvidersProps) {
     <QueryClientProvider client={queryClient}>
       <BrowserRouter>
         <ThemeProvider>
-          <AuthProvider>
-            <ToastProvider>{children}</ToastProvider>
-          </AuthProvider>
+          <ToastProvider>{children}</ToastProvider>
         </ThemeProvider>
       </BrowserRouter>
     </QueryClientProvider>

+ 4 - 121
frontend/src/api/client.ts

@@ -1,47 +1,19 @@
 const API_BASE = '/api/v1';
 
-// Auth token storage
-let authToken: string | null = localStorage.getItem('auth_token');
-
-export function setAuthToken(token: string | null) {
-  authToken = token;
-  if (token) {
-    localStorage.setItem('auth_token', token);
-  } else {
-    localStorage.removeItem('auth_token');
-  }
-}
-
-export function getAuthToken(): string | null {
-  return authToken;
-}
-
 async function request<T>(
   endpoint: string,
   options: RequestInit = {}
 ): Promise<T> {
-  const headers: Record<string, string> = {
-    'Content-Type': 'application/json',
-    ...options.headers as Record<string, string>,
-  };
-
-  // Add auth token if available
-  if (authToken) {
-    headers['Authorization'] = `Bearer ${authToken}`;
-  }
-
   const response = await fetch(`${API_BASE}${endpoint}`, {
     ...options,
     cache: 'no-store', // Prevent browser caching of API responses
-    headers,
+    headers: {
+      'Content-Type': 'application/json',
+      ...options.headers,
+    },
   });
 
   if (!response.ok) {
-    // Handle 401 Unauthorized - clear token and redirect to login
-    if (response.status === 401) {
-      setAuthToken(null);
-      // Don't throw here - let the auth context handle redirect
-    }
     const error = await response.json().catch(() => ({}));
     const detail = error.detail;
     const message = typeof detail === 'string'
@@ -1403,97 +1375,8 @@ export interface ExternalLinkUpdate {
   icon?: string;
 }
 
-// Auth types
-export interface LoginRequest {
-  username: string;
-  password: string;
-}
-
-export interface LoginResponse {
-  access_token: string;
-  token_type: string;
-  user: UserResponse;
-}
-
-export interface UserResponse {
-  id: number;
-  username: string;
-  role: string;
-  is_active: boolean;
-  created_at: string;
-}
-
-export interface UserCreate {
-  username: string;
-  password: string;
-  role: string;
-}
-
-export interface UserUpdate {
-  username?: string;
-  password?: string;
-  role?: string;
-  is_active?: boolean;
-}
-
-export interface SetupRequest {
-  auth_enabled: boolean;
-  admin_username?: string;
-  admin_password?: string;
-}
-
-export interface SetupResponse {
-  auth_enabled: boolean;
-  admin_created?: boolean;
-}
-
-export interface AuthStatus {
-  auth_enabled: boolean;
-  requires_setup: boolean;
-}
-
 // API functions
 export const api = {
-  // Authentication
-  getAuthStatus: () => request<AuthStatus>('/auth/status'),
-  setupAuth: (data: SetupRequest) =>
-    request<SetupResponse>('/auth/setup', {
-      method: 'POST',
-      body: JSON.stringify(data),
-    }),
-  login: (data: LoginRequest) =>
-    request<LoginResponse>('/auth/login', {
-      method: 'POST',
-      body: JSON.stringify(data),
-    }),
-  logout: () =>
-    request<{ message: string }>('/auth/logout', {
-      method: 'POST',
-    }),
-  getCurrentUser: () => request<UserResponse>('/auth/me'),
-  disableAuth: () =>
-    request<{ message: string; auth_enabled: boolean }>('/auth/disable', {
-      method: 'POST',
-    }),
-
-  // Users (admin only)
-  getUsers: () => request<UserResponse[]>('/users/'),
-  getUser: (id: number) => request<UserResponse>(`/users/${id}`),
-  createUser: (data: UserCreate) =>
-    request<UserResponse>('/users/', {
-      method: 'POST',
-      body: JSON.stringify(data),
-    }),
-  updateUser: (id: number, data: UserUpdate) =>
-    request<UserResponse>(`/users/${id}`, {
-      method: 'PATCH',
-      body: JSON.stringify(data),
-    }),
-  deleteUser: (id: number) =>
-    request<void>(`/users/${id}`, {
-      method: 'DELETE',
-    }),
-
   // Printers
   getPrinters: () => request<Printer[]>('/printers/'),
   getPrinter: (id: number) => request<Printer>(`/printers/${id}`),

+ 1 - 33
frontend/src/components/Layout.tsx

@@ -1,6 +1,6 @@
 import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
 import { NavLink, Outlet, useNavigate, useLocation } from 'react-router-dom';
-import { Printer, Archive, Calendar, BarChart3, Cloud, Settings, Sun, Moon, ChevronLeft, ChevronRight, Keyboard, Github, GripVertical, ArrowUpCircle, Wrench, FolderKanban, FolderOpen, X, Menu, Info, Plug, Bug, LogOut, type LucideIcon } from 'lucide-react';
+import { Printer, Archive, Calendar, BarChart3, Cloud, Settings, Sun, Moon, ChevronLeft, ChevronRight, Keyboard, Github, GripVertical, ArrowUpCircle, Wrench, FolderKanban, FolderOpen, X, Menu, Info, Plug, Bug, type LucideIcon } from 'lucide-react';
 import { useTranslation } from 'react-i18next';
 import { useTheme } from '../contexts/ThemeContext';
 import { KeyboardShortcutsModal } from './KeyboardShortcutsModal';
@@ -9,7 +9,6 @@ import { useQuery } from '@tanstack/react-query';
 import { api, supportApi, pendingUploadsApi } from '../api/client';
 import { getIconByName } from './IconPicker';
 import { useIsMobile } from '../hooks/useIsMobile';
-import { useAuth } from '../contexts/AuthContext';
 
 interface NavItem {
   id: string;
@@ -69,7 +68,6 @@ export function Layout() {
   const { mode, toggleMode } = useTheme();
   const { t } = useTranslation();
   const isMobile = useIsMobile();
-  const { user, authEnabled, logout } = useAuth();
   const [sidebarExpanded, setSidebarExpanded] = useState(() => {
     const stored = localStorage.getItem('sidebarExpanded');
     return stored !== 'false';
@@ -170,20 +168,12 @@ export function Layout() {
   const extLinksMap = useMemo(() => new Map((externalLinks || []).map(link => [`ext-${link.id}`, link])), [externalLinks]);
 
   // Compute the ordered sidebar: include stored order + any new items
-  // Filter out 'settings' for users with 'user' role
   const orderedSidebarIds = (() => {
     const result: string[] = [];
     const seen = new Set<string>();
-    
-    // Determine if settings should be hidden (user role and auth enabled)
-    const hideSettings = authEnabled && user?.role === 'user';
 
     // Add items in stored order
     for (const id of sidebarOrder) {
-      // Skip settings if user is not admin
-      if (hideSettings && id === 'settings') {
-        continue;
-      }
       if (navItemsMap.has(id) || extLinksMap.has(id)) {
         result.push(id);
         seen.add(id);
@@ -192,10 +182,6 @@ export function Layout() {
 
     // Add any new internal nav items not in stored order
     for (const item of defaultNavItems) {
-      // Skip settings if user is not admin
-      if (hideSettings && item.id === 'settings') {
-        continue;
-      }
       if (!seen.has(item.id)) {
         result.push(item.id);
         seen.add(item.id);
@@ -579,15 +565,6 @@ export function Layout() {
                 >
                   {mode === 'dark' ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
                 </button>
-                {authEnabled && user && (
-                  <button
-                    onClick={logout}
-                    className="p-2 rounded-lg hover:bg-bambu-dark-tertiary transition-colors text-bambu-gray-light hover:text-white"
-                    title={t('nav.logout', { defaultValue: 'Logout' })}
-                  >
-                    <LogOut className="w-5 h-5" />
-                  </button>
-                )}
               </div>
               {/* Bottom row: version */}
               <div className="flex items-center justify-center gap-2">
@@ -665,15 +642,6 @@ export function Layout() {
               >
                 {mode === 'dark' ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
               </button>
-              {authEnabled && user && (
-                <button
-                  onClick={logout}
-                  className="p-2 rounded-lg hover:bg-bambu-dark-tertiary transition-colors text-bambu-gray-light hover:text-white"
-                  title={t('nav.logout', { defaultValue: 'Logout' })}
-                >
-                  <LogOut className="w-5 h-5" />
-                </button>
-              )}
             </div>
           )}
         </div>

+ 0 - 116
frontend/src/contexts/AuthContext.tsx

@@ -1,116 +0,0 @@
-import React, { createContext, useContext, useEffect, useState } from 'react';
-import { api, getAuthToken, setAuthToken } from '../api/client';
-import type { UserResponse } from '../api/client';
-
-interface AuthContextType {
-  user: UserResponse | null;
-  authEnabled: boolean;
-  loading: boolean;
-  login: (username: string, password: string) => Promise<void>;
-  logout: () => void;
-  refreshUser: () => Promise<void>;
-  refreshAuth: () => Promise<void>;
-}
-
-const AuthContext = createContext<AuthContextType | undefined>(undefined);
-
-export function AuthProvider({ children }: { children: React.ReactNode }) {
-  const [user, setUser] = useState<UserResponse | null>(null);
-  const [authEnabled, setAuthEnabled] = useState(false);
-  const [loading, setLoading] = useState(true);
-
-  const checkAuthStatus = async () => {
-    try {
-      const status = await api.getAuthStatus();
-      setAuthEnabled(status.auth_enabled);
-
-      if (status.auth_enabled) {
-        const token = getAuthToken();
-        if (token) {
-          try {
-            const currentUser = await api.getCurrentUser();
-            setUser(currentUser);
-          } catch {
-            // Token invalid, clear it
-            setAuthToken(null);
-            setUser(null);
-          }
-        } else {
-          setUser(null);
-        }
-      } else {
-        // Auth not enabled, allow access
-        setUser(null);
-        // Check if setup is needed
-        if (status.requires_setup && window.location.pathname !== '/setup') {
-          window.location.href = '/setup';
-        }
-      }
-    } catch (error) {
-      console.error('Failed to check auth status:', error);
-      setAuthEnabled(false);
-      setUser(null);
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  useEffect(() => {
-    checkAuthStatus();
-  }, []);
-
-  const login = async (username: string, password: string) => {
-    const response = await api.login({ username, password });
-    setAuthToken(response.access_token);
-    setUser(response.user);
-  };
-
-  const logout = () => {
-    setAuthToken(null);
-    setUser(null);
-    api.logout().catch(() => {
-      // Ignore logout errors
-    });
-    window.location.href = '/login';
-  };
-
-  const refreshUser = async () => {
-    if (authEnabled && getAuthToken()) {
-      try {
-        const currentUser = await api.getCurrentUser();
-        setUser(currentUser);
-      } catch {
-        setAuthToken(null);
-        setUser(null);
-      }
-    }
-  };
-
-  const refreshAuth = async () => {
-    await checkAuthStatus();
-  };
-
-  return (
-    <AuthContext.Provider
-      value={{
-        user,
-        authEnabled,
-        loading,
-        login,
-        logout,
-        refreshUser,
-        refreshAuth,
-      }}
-    >
-      {children}
-    </AuthContext.Provider>
-  );
-}
-
-export function useAuth() {
-  const context = useContext(AuthContext);
-  if (context === undefined) {
-    throw new Error('useAuth must be used within an AuthProvider');
-  }
-  return context;
-}

+ 0 - 103
frontend/src/pages/LoginPage.tsx

@@ -1,103 +0,0 @@
-import { useState } from 'react';
-import { useNavigate } from 'react-router-dom';
-import { useMutation } from '@tanstack/react-query';
-import { useAuth } from '../contexts/AuthContext';
-import { useToast } from '../contexts/ToastContext';
-import { useTheme } from '../contexts/ThemeContext';
-
-export function LoginPage() {
-  const navigate = useNavigate();
-  const { login } = useAuth();
-  const { showToast } = useToast();
-  const { mode } = useTheme();
-  const [username, setUsername] = useState('');
-  const [password, setPassword] = useState('');
-
-  const loginMutation = useMutation({
-    mutationFn: () => login(username, password),
-    onSuccess: () => {
-      showToast('Logged in successfully');
-      navigate('/');
-    },
-    onError: (error: Error) => {
-      showToast(error.message || 'Login failed', 'error');
-    },
-  });
-
-  const handleSubmit = (e: React.FormEvent) => {
-    e.preventDefault();
-    if (!username || !password) {
-      showToast('Please enter username and password', 'error');
-      return;
-    }
-    loginMutation.mutate();
-  };
-
-  return (
-    <div className="min-h-screen flex items-center justify-center bg-bambu-dark p-4">
-      <div className="max-w-md w-full space-y-8 p-8 bg-gradient-to-br from-bambu-card to-bambu-dark-secondary rounded-xl border border-bambu-dark-tertiary shadow-lg">
-        <div className="text-center">
-          <div className="flex items-center justify-center mb-6">
-            <img
-              src={mode === 'dark' ? '/img/bambuddy_logo_dark_transparent.png' : '/img/bambuddy_logo_light.png'}
-              alt="Bambuddy"
-              className="h-16"
-            />
-          </div>
-          <h2 className="text-3xl font-bold text-white">
-            Bambuddy Login
-          </h2>
-          <p className="mt-2 text-sm text-bambu-gray">
-            Sign in to your account
-          </p>
-        </div>
-
-        <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
-          <div className="space-y-4">
-            <div>
-              <label htmlFor="username" className="block text-sm font-medium text-white mb-2">
-                Username
-              </label>
-              <input
-                id="username"
-                type="text"
-                required
-                value={username}
-                onChange={(e) => setUsername(e.target.value)}
-                className="block w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
-                placeholder="Enter your username"
-                autoComplete="username"
-              />
-            </div>
-
-            <div>
-              <label htmlFor="password" className="block text-sm font-medium text-white mb-2">
-                Password
-              </label>
-              <input
-                id="password"
-                type="password"
-                required
-                value={password}
-                onChange={(e) => setPassword(e.target.value)}
-                className="block w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
-                placeholder="Enter your password"
-                autoComplete="current-password"
-              />
-            </div>
-          </div>
-
-          <div>
-            <button
-              type="submit"
-              disabled={loginMutation.isPending}
-              className="w-full flex justify-center py-3 px-4 bg-bambu-green hover:bg-bambu-green-light text-white font-medium rounded-lg shadow-lg shadow-bambu-green/20 hover:shadow-bambu-green/30 focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:ring-offset-2 focus:ring-offset-bambu-dark-secondary transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-bambu-green"
-            >
-              {loginMutation.isPending ? 'Logging in...' : 'Sign in'}
-            </button>
-          </div>
-        </form>
-      </div>
-    </div>
-  );
-}

+ 3 - 242
frontend/src/pages/SettingsPage.tsx

@@ -1,9 +1,7 @@
 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { Loader2, Plus, Plug, AlertTriangle, RotateCcw, Bell, Download, RefreshCw, ExternalLink, Globe, Droplets, Thermometer, FileText, Edit2, Send, CheckCircle, XCircle, History, Trash2, Upload, Zap, TrendingUp, Calendar, DollarSign, Power, PowerOff, Key, Copy, Database, Info, X, Shield, Printer, Cylinder, Wifi, Home, Video, Users, Lock, Unlock } from 'lucide-react';
+import { Loader2, Plus, Plug, AlertTriangle, RotateCcw, Bell, Download, RefreshCw, ExternalLink, Globe, Droplets, Thermometer, FileText, Edit2, Send, CheckCircle, XCircle, History, Trash2, Upload, Zap, TrendingUp, Calendar, DollarSign, Power, PowerOff, Key, Copy, Database, Info, X, Shield, Printer, Cylinder, Wifi, Home, Video } from 'lucide-react';
 import { useTranslation } from 'react-i18next';
-import { useNavigate } from 'react-router-dom';
 import { api } from '../api/client';
-import { useAuth } from '../contexts/AuthContext';
 import { formatDateOnly } from '../utils/date';
 import type { AppSettings, AppSettingsUpdate, SmartPlug, SmartPlugStatus, NotificationProvider, NotificationTemplate, UpdateStatus } from '../api/client';
 import { Card, CardContent, CardHeader } from '../components/Card';
@@ -31,10 +29,8 @@ import { Palette } from 'lucide-react';
 
 export function SettingsPage() {
   const queryClient = useQueryClient();
-  const navigate = useNavigate();
   const { t, i18n } = useTranslation();
   const { showToast, showPersistentToast, dismissToast } = useToast();
-  const { authEnabled, user, refreshAuth } = useAuth();
   const {
     mode,
     darkStyle, darkBackground, darkAccent,
@@ -50,7 +46,7 @@ export function SettingsPage() {
   const [editingTemplate, setEditingTemplate] = useState<NotificationTemplate | null>(null);
   const [showLogViewer, setShowLogViewer] = useState(false);
   const [defaultView, setDefaultViewState] = useState<string>(getDefaultView());
-  const [activeTab, setActiveTab] = useState<'general' | 'network' | 'plugs' | 'notifications' | 'filament' | 'apikeys' | 'virtual-printer' | 'users'>('general');
+  const [activeTab, setActiveTab] = useState<'general' | 'network' | 'plugs' | 'notifications' | 'filament' | 'apikeys' | 'virtual-printer'>('general');
   const [showCreateAPIKey, setShowCreateAPIKey] = useState(false);
   const [newAPIKeyName, setNewAPIKeyName] = useState('');
   const [newAPIKeyPermissions, setNewAPIKeyPermissions] = useState({
@@ -70,7 +66,6 @@ export function SettingsPage() {
   const [showRestoreModal, setShowRestoreModal] = useState(false);
   const [showTelemetryInfo, setShowTelemetryInfo] = useState(false);
   const [showReleaseNotes, setShowReleaseNotes] = useState(false);
-  const [showDisableAuthConfirm, setShowDisableAuthConfirm] = useState(false);
 
   // Home Assistant test connection state
   const [haTestResult, setHaTestResult] = useState<{ success: boolean; message: string | null; error: string | null } | null>(null);
@@ -479,7 +474,7 @@ export function SettingsPage() {
       </div>
 
       {/* Tab Navigation */}
-      <div className="flex gap-1 mb-6 border-b border-bambu-dark-tertiary overflow-x-auto">
+      <div className="flex gap-1 mb-6 border-b border-bambu-dark-tertiary">
         <button
           onClick={() => setActiveTab('general')}
           className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
@@ -573,20 +568,6 @@ export function SettingsPage() {
           Virtual Printer
           <span className={`w-2 h-2 rounded-full ${virtualPrinterRunning ? 'bg-green-400' : 'bg-gray-500'}`} />
         </button>
-        <button
-          onClick={() => setActiveTab('users')}
-          className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
-            activeTab === 'users'
-              ? 'text-bambu-green border-bambu-green'
-              : 'text-bambu-gray hover:text-gray-900 dark:hover:text-white border-transparent'
-          }`}
-        >
-          <Users className="w-4 h-4" />
-          Users
-          {authEnabled && (
-            <span className={`w-2 h-2 rounded-full ${authEnabled ? 'bg-green-400' : 'bg-gray-500'}`} />
-          )}
-        </button>
       </div>
 
       {/* General Tab */}
@@ -2885,226 +2866,6 @@ export function SettingsPage() {
           </Card>
         </div>
       )}
-
-      {/* Users Tab */}
-      {activeTab === 'users' && (
-        <div className="grid grid-cols-1 xl:grid-cols-2 gap-8">
-          <div>
-            <div className="mb-6">
-              <h2 className="text-lg font-semibold text-white flex items-center gap-2">
-                <Users className="w-5 h-5 text-bambu-green" />
-                User Authentication
-              </h2>
-              <p className="text-sm text-bambu-gray mt-1">
-                Enable authentication to secure your Bambuddy instance and manage user access.
-              </p>
-            </div>
-
-            <Card>
-              <CardContent className="py-6">
-                {!authEnabled ? (
-                  <div className="space-y-4">
-                    <div className="flex items-center gap-3">
-                      <div className={`w-12 h-12 rounded-full flex items-center justify-center ${authEnabled ? 'bg-green-500/20' : 'bg-gray-500/20'}`}>
-                        {authEnabled ? (
-                          <Lock className="w-6 h-6 text-green-400" />
-                        ) : (
-                          <Unlock className="w-6 h-6 text-gray-400" />
-                        )}
-                      </div>
-                      <div className="flex-1">
-                        <h3 className="text-white font-medium">Authentication Disabled</h3>
-                        <p className="text-sm text-bambu-gray">
-                          Your Bambuddy instance is currently accessible without authentication.
-                        </p>
-                      </div>
-                    </div>
-
-                    <div className="pt-4 border-t border-bambu-dark-tertiary">
-                      <p className="text-sm text-bambu-gray mb-4">
-                        Enable authentication to:
-                      </p>
-                      <ul className="space-y-2 text-sm text-bambu-gray mb-4">
-                        <li className="flex items-start gap-2">
-                          <CheckCircle className="w-4 h-4 text-bambu-green mt-0.5 flex-shrink-0" />
-                          <span>Require login to access the system</span>
-                        </li>
-                        <li className="flex items-start gap-2">
-                          <CheckCircle className="w-4 h-4 text-bambu-green mt-0.5 flex-shrink-0" />
-                          <span>Manage multiple users with different roles</span>
-                        </li>
-                        <li className="flex items-start gap-2">
-                          <CheckCircle className="w-4 h-4 text-bambu-green mt-0.5 flex-shrink-0" />
-                          <span>Control access to printer settings and user management</span>
-                        </li>
-                      </ul>
-
-                      <Button
-                        onClick={() => navigate('/setup')}
-                        className="w-full"
-                      >
-                        <Lock className="w-4 h-4" />
-                        Activate Authentication
-                      </Button>
-                    </div>
-                  </div>
-                ) : (
-                  <div className="space-y-4">
-                    <div className="flex items-center gap-3">
-                      <div className="w-12 h-12 rounded-full flex items-center justify-center bg-green-500/20">
-                        <Lock className="w-6 h-6 text-green-400" />
-                      </div>
-                      <div className="flex-1">
-                        <h3 className="text-white font-medium">Authentication Enabled</h3>
-                        <p className="text-sm text-bambu-gray">
-                          Your Bambuddy instance is secured with authentication.
-                        </p>
-                      </div>
-                    </div>
-
-                    {user && (
-                      <div className="pt-4 border-t border-bambu-dark-tertiary">
-                        <div className="flex items-center justify-between mb-4">
-                          <div>
-                            <p className="text-sm text-bambu-gray">Current User</p>
-                            <p className="text-white font-medium">{user.username}</p>
-                            <p className="text-xs text-bambu-gray mt-1">
-                              Role: <span className="capitalize">{user.role}</span>
-                            </p>
-                          </div>
-                          <div className={`px-3 py-1 rounded-full text-xs font-medium ${
-                            user.role === 'admin' 
-                              ? 'bg-purple-500/20 text-purple-300' 
-                              : 'bg-blue-500/20 text-blue-300'
-                          }`}>
-                            {user.role === 'admin' ? 'Admin' : 'User'}
-                          </div>
-                        </div>
-                      </div>
-                    )}
-
-                    <div className="pt-4 border-t border-bambu-dark-tertiary space-y-3">
-                      <Button
-                        onClick={() => navigate('/users')}
-                        className="w-full"
-                        variant="secondary"
-                      >
-                        <Users className="w-4 h-4" />
-                        Manage Users
-                      </Button>
-                      
-                      {user?.role === 'admin' && (
-                        <Button
-                          onClick={() => setShowDisableAuthConfirm(true)}
-                          className="w-full"
-                          variant="secondary"
-                        >
-                          <Unlock className="w-4 h-4" />
-                          Disable Authentication
-                        </Button>
-                      )}
-                    </div>
-                  </div>
-                )}
-              </CardContent>
-            </Card>
-          </div>
-
-          {authEnabled && (
-            <div>
-              <div className="mb-6">
-                <h2 className="text-lg font-semibold text-white flex items-center gap-2">
-                  <Shield className="w-5 h-5 text-bambu-green" />
-                  Role Permissions
-                </h2>
-                <p className="text-sm text-bambu-gray mt-1">
-                  Overview of what each role can do.
-                </p>
-              </div>
-
-              <div className="space-y-4">
-                <Card>
-                  <CardHeader>
-                    <div className="flex items-center gap-2">
-                      <div className="w-8 h-8 rounded-full bg-purple-500/20 flex items-center justify-center">
-                        <Shield className="w-4 h-4 text-purple-300" />
-                      </div>
-                      <h3 className="text-white font-medium">Admin</h3>
-                    </div>
-                  </CardHeader>
-                  <CardContent>
-                    <ul className="space-y-2 text-sm text-bambu-gray">
-                      <li className="flex items-start gap-2">
-                        <CheckCircle className="w-4 h-4 text-bambu-green mt-0.5 flex-shrink-0" />
-                        <span>Manage printer settings</span>
-                      </li>
-                      <li className="flex items-start gap-2">
-                        <CheckCircle className="w-4 h-4 text-bambu-green mt-0.5 flex-shrink-0" />
-                        <span>Create, edit, and delete users</span>
-                      </li>
-                      <li className="flex items-start gap-2">
-                        <CheckCircle className="w-4 h-4 text-bambu-green mt-0.5 flex-shrink-0" />
-                        <span>Access all system features</span>
-                      </li>
-                    </ul>
-                  </CardContent>
-                </Card>
-
-                <Card>
-                  <CardHeader>
-                    <div className="flex items-center gap-2">
-                      <div className="w-8 h-8 rounded-full bg-blue-500/20 flex items-center justify-center">
-                        <Users className="w-4 h-4 text-blue-300" />
-                      </div>
-                      <h3 className="text-white font-medium">User</h3>
-                    </div>
-                  </CardHeader>
-                  <CardContent>
-                    <ul className="space-y-2 text-sm text-bambu-gray">
-                      <li className="flex items-start gap-2">
-                        <CheckCircle className="w-4 h-4 text-bambu-green mt-0.5 flex-shrink-0" />
-                        <span>Send print jobs</span>
-                      </li>
-                      <li className="flex items-start gap-2">
-                        <CheckCircle className="w-4 h-4 text-bambu-green mt-0.5 flex-shrink-0" />
-                        <span>Manage files and archives</span>
-                      </li>
-                      <li className="flex items-start gap-2">
-                        <CheckCircle className="w-4 h-4 text-bambu-green mt-0.5 flex-shrink-0" />
-                        <span>Manage filament</span>
-                      </li>
-                    </ul>
-                  </CardContent>
-                </Card>
-              </div>
-            </div>
-          )}
-        </div>
-      )}
-
-      {/* Disable Authentication Confirmation Modal */}
-      {showDisableAuthConfirm && (
-        <ConfirmModal
-          title="Disable Authentication"
-          message="Are you sure you want to disable authentication? This will make your Bambuddy instance accessible without login. All users will remain in the database but authentication will be disabled."
-          confirmText="Disable Authentication"
-          variant="danger"
-          onConfirm={async () => {
-            try {
-              await api.disableAuth();
-              showToast('Authentication disabled successfully', 'success');
-              await refreshAuth();
-              setShowDisableAuthConfirm(false);
-              // Refresh the page to ensure all protected routes are accessible
-              window.location.href = '/';
-            } catch (error: unknown) {
-              const message = error instanceof Error ? error.message : 'Failed to disable authentication';
-              showToast(message, 'error');
-            }
-          }}
-          onCancel={() => setShowDisableAuthConfirm(false)}
-        />
-      )}
     </div>
   );
 }

+ 0 - 161
frontend/src/pages/SetupPage.tsx

@@ -1,161 +0,0 @@
-import { useState } from 'react';
-import { useNavigate } from 'react-router-dom';
-import { useMutation } from '@tanstack/react-query';
-import { api } from '../api/client';
-import { useToast } from '../contexts/ToastContext';
-import { useTheme } from '../contexts/ThemeContext';
-
-export function SetupPage() {
-  const navigate = useNavigate();
-  const { showToast } = useToast();
-  const { mode } = useTheme();
-  const [authEnabled, setAuthEnabled] = useState(false);
-  const [adminUsername, setAdminUsername] = useState('');
-  const [adminPassword, setAdminPassword] = useState('');
-  const [confirmPassword, setConfirmPassword] = useState('');
-
-  const setupMutation = useMutation({
-    mutationFn: () =>
-      api.setupAuth({
-        auth_enabled: authEnabled,
-        admin_username: authEnabled ? adminUsername : undefined,
-        admin_password: authEnabled ? adminPassword : undefined,
-      }),
-    onSuccess: (data) => {
-      if (data.auth_enabled && data.admin_created) {
-        showToast('Authentication enabled and admin user created');
-        navigate('/login');
-      } else {
-        showToast('Setup completed');
-        navigate('/');
-      }
-    },
-    onError: (error: Error) => {
-      showToast(error.message, 'error');
-    },
-  });
-
-  const handleSubmit = (e: React.FormEvent) => {
-    e.preventDefault();
-
-    if (authEnabled) {
-      if (!adminUsername || !adminPassword) {
-        showToast('Please enter admin username and password', 'error');
-        return;
-      }
-      if (adminPassword !== confirmPassword) {
-        showToast('Passwords do not match', 'error');
-        return;
-      }
-      if (adminPassword.length < 6) {
-        showToast('Password must be at least 6 characters', 'error');
-        return;
-      }
-    }
-
-    setupMutation.mutate();
-  };
-
-  return (
-    <div className="min-h-screen flex items-center justify-center bg-bambu-dark p-4">
-      <div className="max-w-md w-full space-y-8 p-8 bg-gradient-to-br from-bambu-card to-bambu-dark-secondary rounded-xl border border-bambu-dark-tertiary shadow-lg">
-        <div className="text-center">
-          <div className="flex items-center justify-center mb-6">
-            <img
-              src={mode === 'dark' ? '/img/bambuddy_logo_dark_transparent.png' : '/img/bambuddy_logo_light.png'}
-              alt="Bambuddy"
-              className="h-16"
-            />
-          </div>
-          <h2 className="text-3xl font-bold text-white">
-            Bambuddy Setup
-          </h2>
-          <p className="mt-2 text-sm text-bambu-gray">
-            Configure authentication for your Bambuddy instance
-          </p>
-        </div>
-
-        <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
-          <div className="space-y-4">
-            <div className="flex items-center p-4 bg-bambu-dark-secondary/50 rounded-lg border border-bambu-dark-tertiary">
-              <input
-                id="auth-enabled"
-                type="checkbox"
-                checked={authEnabled}
-                onChange={(e) => setAuthEnabled(e.target.checked)}
-                className="h-4 w-4 text-bambu-green focus:ring-bambu-green border-bambu-dark-tertiary rounded bg-bambu-dark-secondary"
-              />
-              <label htmlFor="auth-enabled" className="ml-3 block text-sm font-medium text-white">
-                Enable Authentication
-              </label>
-            </div>
-
-            {authEnabled && (
-              <div className="space-y-4 mt-4">
-                <div>
-                  <label htmlFor="admin-username" className="block text-sm font-medium text-white mb-2">
-                    Admin Username
-                  </label>
-                  <input
-                    id="admin-username"
-                    type="text"
-                    required
-                    value={adminUsername}
-                    onChange={(e) => setAdminUsername(e.target.value)}
-                    className="block w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
-                    placeholder="Enter admin username"
-                    autoComplete="username"
-                  />
-                </div>
-
-                <div>
-                  <label htmlFor="admin-password" className="block text-sm font-medium text-white mb-2">
-                    Admin Password
-                  </label>
-                  <input
-                    id="admin-password"
-                    type="password"
-                    required
-                    value={adminPassword}
-                    onChange={(e) => setAdminPassword(e.target.value)}
-                    className="block w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
-                    placeholder="Enter admin password"
-                    minLength={6}
-                    autoComplete="new-password"
-                  />
-                </div>
-
-                <div>
-                  <label htmlFor="confirm-password" className="block text-sm font-medium text-white mb-2">
-                    Confirm Password
-                  </label>
-                  <input
-                    id="confirm-password"
-                    type="password"
-                    required
-                    value={confirmPassword}
-                    onChange={(e) => setConfirmPassword(e.target.value)}
-                    className="block w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
-                    placeholder="Confirm admin password"
-                    minLength={6}
-                    autoComplete="new-password"
-                  />
-                </div>
-              </div>
-            )}
-          </div>
-
-          <div>
-            <button
-              type="submit"
-              disabled={setupMutation.isPending}
-              className="w-full flex justify-center py-3 px-4 bg-bambu-green hover:bg-bambu-green-light text-white font-medium rounded-lg shadow-lg shadow-bambu-green/20 hover:shadow-bambu-green/30 focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:ring-offset-2 focus:ring-offset-bambu-dark-secondary transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-bambu-green"
-            >
-              {setupMutation.isPending ? 'Setting up...' : 'Complete Setup'}
-            </button>
-          </div>
-        </form>
-      </div>
-    </div>
-  );
-}

+ 0 - 398
frontend/src/pages/UsersPage.tsx

@@ -1,398 +0,0 @@
-import { useState, useEffect } from 'react';
-import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-import { X, Plus, Edit2, Trash2, Save, Loader2, Users as UsersIcon, Shield } from 'lucide-react';
-import { api } from '../api/client';
-import type { UserCreate, UserUpdate } from '../api/client';
-import { useAuth } from '../contexts/AuthContext';
-import { useToast } from '../contexts/ToastContext';
-import { Button } from '../components/Button';
-import { Card, CardContent, CardHeader } from '../components/Card';
-import { ConfirmModal } from '../components/ConfirmModal';
-
-export function UsersPage() {
-  const { user: currentUser } = useAuth();
-  const { showToast } = useToast();
-  const queryClient = useQueryClient();
-  const [showCreateModal, setShowCreateModal] = useState(false);
-  const [editingUser, setEditingUser] = useState<number | null>(null);
-  const [deleteUserId, setDeleteUserId] = useState<number | null>(null);
-  const [formData, setFormData] = useState<UserCreate>({
-    username: '',
-    password: '',
-    role: 'user',
-  });
-
-  // Close modal on Escape key
-  useEffect(() => {
-    const handleKeyDown = (e: KeyboardEvent) => {
-      if (e.key === 'Escape' && showCreateModal) {
-        setShowCreateModal(false);
-        setFormData({ username: '', password: '', role: 'user' });
-      }
-    };
-    window.addEventListener('keydown', handleKeyDown);
-    return () => window.removeEventListener('keydown', handleKeyDown);
-  }, [showCreateModal]);
-
-  const { data: users = [], isLoading } = useQuery({
-    queryKey: ['users'],
-    queryFn: () => api.getUsers(),
-  });
-
-  const createMutation = useMutation({
-    mutationFn: (data: UserCreate) => api.createUser(data),
-    onSuccess: () => {
-      queryClient.invalidateQueries({ queryKey: ['users'] });
-      setShowCreateModal(false);
-      setFormData({ username: '', password: '', role: 'user' });
-      showToast('User created successfully');
-    },
-    onError: (error: Error) => {
-      showToast(error.message, 'error');
-    },
-  });
-
-  const updateMutation = useMutation({
-    mutationFn: ({ id, data }: { id: number; data: UserUpdate }) => api.updateUser(id, data),
-    onSuccess: () => {
-      queryClient.invalidateQueries({ queryKey: ['users'] });
-      setEditingUser(null);
-      setFormData({ username: '', password: '', role: 'user' });
-      showToast('User updated successfully');
-    },
-    onError: (error: Error) => {
-      showToast(error.message, 'error');
-    },
-  });
-
-  const deleteMutation = useMutation({
-    mutationFn: (id: number) => api.deleteUser(id),
-    onSuccess: () => {
-      queryClient.invalidateQueries({ queryKey: ['users'] });
-      showToast('User deleted successfully');
-    },
-    onError: (error: Error) => {
-      showToast(error.message, 'error');
-    },
-  });
-
-  const handleCreate = () => {
-    if (!formData.username || !formData.password) {
-      showToast('Please fill in all required fields', 'error');
-      return;
-    }
-    createMutation.mutate(formData);
-  };
-
-  const handleUpdate = (id: number) => {
-    const updateData: UserUpdate = {
-      username: formData.username || undefined,
-      password: formData.password || undefined,
-      role: formData.role,
-    };
-    // Remove password if empty
-    if (!updateData.password) {
-      delete updateData.password;
-    }
-    updateMutation.mutate({ id, data: updateData });
-  };
-
-  const handleDelete = (id: number) => {
-    setDeleteUserId(id);
-  };
-
-  const startEdit = (user: { id: number; username: string; role: string }) => {
-    setEditingUser(user.id);
-    setFormData({
-      username: user.username,
-      password: '',
-      role: user.role,
-    });
-  };
-
-  if (currentUser?.role !== 'admin') {
-    return (
-      <div className="p-6">
-        <Card>
-          <CardContent className="py-6">
-            <div className="flex items-center gap-3 text-red-400">
-              <Shield className="w-5 h-5" />
-              <p className="text-white">You do not have permission to access this page.</p>
-            </div>
-          </CardContent>
-        </Card>
-      </div>
-    );
-  }
-
-  return (
-    <div className="p-6">
-      <div className="flex justify-between items-center mb-6">
-        <div>
-          <h1 className="text-2xl font-bold text-white flex items-center gap-2">
-            <UsersIcon className="w-6 h-6 text-bambu-green" />
-            User Management
-          </h1>
-          <p className="text-sm text-bambu-gray mt-1">
-            Manage users and their access to your Bambuddy instance
-          </p>
-        </div>
-        <Button
-          onClick={() => {
-            setShowCreateModal(true);
-            setFormData({ username: '', password: '', role: 'user' });
-          }}
-        >
-          <Plus className="w-4 h-4" />
-          Create User
-        </Button>
-      </div>
-
-      {isLoading ? (
-        <div className="flex items-center justify-center py-12">
-          <Loader2 className="w-8 h-8 text-bambu-green animate-spin" />
-        </div>
-      ) : (
-        <Card>
-          <div className="overflow-x-auto">
-            <table className="min-w-full divide-y divide-bambu-dark-tertiary">
-              <thead>
-                <tr>
-                  <th className="px-6 py-3 text-left text-xs font-medium text-bambu-gray uppercase tracking-wider">
-                    Username
-                  </th>
-                  <th className="px-6 py-3 text-left text-xs font-medium text-bambu-gray uppercase tracking-wider">
-                    Role
-                  </th>
-                  <th className="px-6 py-3 text-left text-xs font-medium text-bambu-gray uppercase tracking-wider">
-                    Status
-                  </th>
-                  <th className="px-6 py-3 text-left text-xs font-medium text-bambu-gray uppercase tracking-wider">
-                    Actions
-                  </th>
-                </tr>
-              </thead>
-              <tbody className="divide-y divide-bambu-dark-tertiary">
-                {users.map((user) => (
-                  <tr key={user.id} className="hover:bg-bambu-dark-tertiary/50 transition-colors">
-                    <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-white">
-                      {editingUser === user.id ? (
-                        <input
-                          type="text"
-                          value={formData.username}
-                          onChange={(e) => setFormData({ ...formData, username: e.target.value })}
-                          className="px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green"
-                        />
-                      ) : (
-                        user.username
-                      )}
-                    </td>
-                    <td className="px-6 py-4 whitespace-nowrap text-sm">
-                      {editingUser === user.id ? (
-                        <select
-                          value={formData.role}
-                          onChange={(e) => setFormData({ ...formData, role: e.target.value as 'admin' | 'user' })}
-                          className="px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green"
-                        >
-                          <option value="user">User</option>
-                          <option value="admin">Admin</option>
-                        </select>
-                      ) : (
-                        <span className={`px-3 py-1 rounded-full text-xs font-medium ${
-                          user.role === 'admin' 
-                            ? 'bg-purple-500/20 text-purple-300' 
-                            : 'bg-blue-500/20 text-blue-300'
-                        }`}>
-                          {user.role}
-                        </span>
-                      )}
-                    </td>
-                    <td className="px-6 py-4 whitespace-nowrap text-sm">
-                      <span className={`px-3 py-1 rounded-full text-xs font-medium ${
-                        user.is_active 
-                          ? 'bg-bambu-green/20 text-bambu-green' 
-                          : 'bg-red-500/20 text-red-400'
-                      }`}>
-                        {user.is_active ? 'Active' : 'Inactive'}
-                      </span>
-                    </td>
-                    <td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
-                      {editingUser === user.id ? (
-                        <div className="flex items-center gap-2">
-                          <Button
-                            size="sm"
-                            onClick={() => handleUpdate(user.id)}
-                            disabled={updateMutation.isPending}
-                          >
-                            {updateMutation.isPending ? (
-                              <Loader2 className="w-4 h-4 animate-spin" />
-                            ) : (
-                              <Save className="w-4 h-4" />
-                            )}
-                            Save
-                          </Button>
-                          <Button
-                            size="sm"
-                            variant="secondary"
-                            onClick={() => {
-                              setEditingUser(null);
-                              setFormData({ username: '', password: '', role: 'user' });
-                            }}
-                          >
-                            Cancel
-                          </Button>
-                        </div>
-                      ) : (
-                        <div className="flex items-center gap-2">
-                          <Button
-                            size="sm"
-                            variant="ghost"
-                            onClick={() => startEdit(user)}
-                          >
-                            <Edit2 className="w-4 h-4" />
-                            Edit
-                          </Button>
-                          {user.id !== currentUser?.id && (
-                            <Button
-                              size="sm"
-                              variant="ghost"
-                              onClick={() => handleDelete(user.id)}
-                            >
-                              <Trash2 className="w-4 h-4" />
-                              Delete
-                            </Button>
-                          )}
-                        </div>
-                      )}
-                    </td>
-                  </tr>
-                ))}
-              </tbody>
-            </table>
-          </div>
-        </Card>
-      )}
-
-      {/* Create User Modal */}
-      {showCreateModal && (
-        <div
-          className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4"
-          onClick={() => {
-            setShowCreateModal(false);
-            setFormData({ username: '', password: '', role: 'user' });
-          }}
-        >
-          <Card
-            className="w-full max-w-md"
-            onClick={(e: React.MouseEvent) => e.stopPropagation()}
-          >
-            <CardHeader>
-              <div className="flex items-center justify-between">
-                <div className="flex items-center gap-2">
-                  <UsersIcon className="w-5 h-5 text-bambu-green" />
-                  <h2 className="text-lg font-semibold text-white">Create User</h2>
-                </div>
-                <Button
-                  variant="ghost"
-                  size="sm"
-                  onClick={() => {
-                    setShowCreateModal(false);
-                    setFormData({ username: '', password: '', role: 'user' });
-                  }}
-                >
-                  <X className="w-5 h-5" />
-                </Button>
-              </div>
-            </CardHeader>
-            <CardContent>
-              <div className="space-y-4">
-                <div>
-                  <label className="block text-sm font-medium text-white mb-2">
-                    Username
-                  </label>
-                  <input
-                    type="text"
-                    value={formData.username}
-                    onChange={(e) => setFormData({ ...formData, username: e.target.value })}
-                    className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
-                    placeholder="Enter username"
-                    autoComplete="username"
-                  />
-                </div>
-                <div>
-                  <label className="block text-sm font-medium text-white mb-2">
-                    Password
-                  </label>
-                  <input
-                    type="password"
-                    value={formData.password}
-                    onChange={(e) => setFormData({ ...formData, password: e.target.value })}
-                    className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
-                    placeholder="Enter password"
-                    autoComplete="new-password"
-                    minLength={6}
-                  />
-                </div>
-                <div>
-                  <label className="block text-sm font-medium text-white mb-2">
-                    Role
-                  </label>
-                  <select
-                    value={formData.role}
-                    onChange={(e) => setFormData({ ...formData, role: e.target.value as 'admin' | 'user' })}
-                    className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
-                  >
-                    <option value="user">User</option>
-                    <option value="admin">Admin</option>
-                  </select>
-                </div>
-              </div>
-              <div className="mt-6 flex justify-end gap-3">
-                <Button
-                  variant="secondary"
-                  onClick={() => {
-                    setShowCreateModal(false);
-                    setFormData({ username: '', password: '', role: 'user' });
-                  }}
-                >
-                  Cancel
-                </Button>
-                <Button
-                  onClick={handleCreate}
-                  disabled={createMutation.isPending || !formData.username || !formData.password}
-                >
-                  {createMutation.isPending ? (
-                    <>
-                      <Loader2 className="w-4 h-4 animate-spin" />
-                      Creating...
-                    </>
-                  ) : (
-                    <>
-                      <Plus className="w-4 h-4" />
-                      Create User
-                    </>
-                  )}
-                </Button>
-              </div>
-            </CardContent>
-          </Card>
-        </div>
-      )}
-
-      {/* Delete Confirmation Modal */}
-      {deleteUserId !== null && (
-        <ConfirmModal
-          title="Delete User"
-          message={`Are you sure you want to delete this user? This action cannot be undone.`}
-          confirmText="Delete User"
-          variant="danger"
-          onConfirm={() => {
-            deleteMutation.mutate(deleteUserId);
-            setDeleteUserId(null);
-          }}
-          onCancel={() => setDeleteUserId(null)}
-        />
-      )}
-    </div>
-  );
-}

+ 0 - 1
frontend/tailwind.config.js

@@ -16,7 +16,6 @@ export default {
           dark: '#1a1a1a',
           'dark-secondary': '#2d2d2d',
           'dark-tertiary': '#3d3d3d',
-          card: '#2d2d2d', // Same as dark-secondary for card backgrounds
           gray: '#808080',
           'gray-light': '#a0a0a0',
           'gray-dark': '#4a4a4a',

+ 0 - 4
requirements.txt

@@ -37,10 +37,6 @@ qrcode[pil]>=7.4.0
 # System monitoring
 psutil>=6.0.0
 
-# Authentication
-python-jose[cryptography]>=3.3.0
-passlib[bcrypt]>=1.7.4
-
 # Development
 pytest>=8.0.0
 pytest-asyncio>=0.23.0

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-BK0uiRXP.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-BmODu1qm.css


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-COZJGA_d.css


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-DFo1_Rau.js


+ 2 - 2
static/index.html

@@ -23,8 +23,8 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-BK0uiRXP.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-COZJGA_d.css">
+    <script type="module" crossorigin src="/assets/index-DFo1_Rau.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-BmODu1qm.css">
   </head>
   <body>
     <div id="root"></div>

Some files were not shown because too many files changed in this diff