From 3a848643c0c7df585bed1e5ed792d5da4ab87939 Mon Sep 17 00:00:00 2001 From: JesseFPV Date: Thu, 22 Jan 2026 12:21:07 +0100 Subject: [PATCH] Fixed re-auth setup and gitignore node modules --- .gitignore | 3 + backend/app/api/routes/auth.py | 102 ++- frontend/src/App.tsx | 3 + frontend/src/contexts/AuthContext.tsx | 29 +- frontend/src/pages/SettingsPage.tsx | 6 +- frontend/src/pages/SetupPage.tsx | 101 ++- .../{index-jyNRggdg.js => index-7nHE42SH.js} | 834 +++++++++--------- static/index.html | 2 +- 8 files changed, 586 insertions(+), 494 deletions(-) rename static/assets/{index-jyNRggdg.js => index-7nHE42SH.js} (68%) diff --git a/.gitignore b/.gitignore index cff918569..f5eb2dfd3 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,6 @@ logs/ *.log* bambutrack.log.* firmware/ + +# Node modules +node_modules/ \ No newline at end of file diff --git a/backend/app/api/routes/auth.py b/backend/app/api/routes/auth.py index 733b9ac94..7b24d558f 100644 --- a/backend/app/api/routes/auth.py +++ b/backend/app/api/routes/auth.py @@ -42,6 +42,26 @@ async def set_auth_enabled(db: AsyncSession, enabled: bool) -> None: # Note: Don't commit here - let get_db handle it or commit explicitly in the route +async def is_setup_completed(db: AsyncSession) -> bool: + """Check if setup has been completed.""" + result = await db.execute(select(Settings).where(Settings.key == "setup_completed")) + setting = result.scalar_one_or_none() + return setting and setting.value.lower() == "true" + + +async def set_setup_completed(db: AsyncSession, completed: bool) -> None: + """Set setup completed status.""" + from sqlalchemy import func + from sqlalchemy.dialects.sqlite import insert as sqlite_insert + + stmt = sqlite_insert(Settings).values(key="setup_completed", value="true" if completed else "false") + stmt = stmt.on_conflict_do_update( + index_elements=["key"], set_={"value": "true" if completed 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.""" @@ -70,48 +90,61 @@ async def setup_auth(request: SetupRequest, db: AsyncSession = Depends(get_db)): 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 users already exist + admin_users_result = await db.execute(select(User).where(User.role == "admin")) + existing_admin_users = list(admin_users_result.scalars().all()) + has_admin_users = len(existing_admin_users) > 0 - # 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", - ) + if has_admin_users: + # Admin users already exist, just enable auth (don't create new admin) + logger.info(f"Admin users already exist ({len(existing_admin_users)} found), enabling authentication without creating new admin") + admin_created = False + else: + # No admin users exist, require admin credentials to create first admin + 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 (no admin users exist)", + ) - # 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)}", - ) + # Check if username already exists (shouldn't happen if no admin users exist, but check anyway) + existing_user = await get_user_by_username(db, request.admin_username) + if existing_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="User with this username already exists", + ) - # Set auth enabled and commit everything together + # 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 mark setup as completed await set_auth_enabled(db, request.auth_enabled) + await set_setup_completed(db, True) await db.commit() if admin_created: await db.refresh(admin_user) logger.info(f"Admin user created successfully: {admin_user.id}") + logger.info(f"Setup completed: auth_enabled={request.auth_enabled}, admin_created={admin_created}") return SetupResponse(auth_enabled=request.auth_enabled, admin_created=admin_created) except HTTPException: raise @@ -128,7 +161,10 @@ async def setup_auth(request: SetupRequest, db: AsyncSession = Depends(get_db)): 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} + setup_completed = await is_setup_completed(db) + # Only require setup if it hasn't been completed yet + requires_setup = not setup_completed + return {"auth_enabled": auth_enabled, "requires_setup": requires_setup} @router.post("/disable", response_model=dict) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 48cc0f158..06efded93 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -82,6 +82,9 @@ function SetupRoute({ children }: { children: React.ReactNode }) { return
Loading...
; } + // If auth is already enabled, redirect to login + // Otherwise, allow access to setup page (even if setup was completed before) + // This allows users to enable auth later if they skipped it during initial setup if (authEnabled) { return ; } diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index 9429dad9b..1fd1ac1db 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -1,10 +1,11 @@ -import React, { createContext, useContext, useEffect, useState } from 'react'; +import React, { createContext, useContext, useEffect, useRef, useState } from 'react'; import { api, getAuthToken, setAuthToken } from '../api/client'; import type { UserResponse } from '../api/client'; interface AuthContextType { user: UserResponse | null; authEnabled: boolean; + requiresSetup: boolean; loading: boolean; login: (username: string, password: string) => Promise; logout: () => void; @@ -17,12 +18,15 @@ const AuthContext = createContext(undefined); export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState(null); const [authEnabled, setAuthEnabled] = useState(false); + const [requiresSetup, setRequiresSetup] = useState(false); const [loading, setLoading] = useState(true); + const hasRedirectedRef = useRef(false); const checkAuthStatus = async () => { try { const status = await api.getAuthStatus(); setAuthEnabled(status.auth_enabled); + setRequiresSetup(status.requires_setup); if (status.auth_enabled) { const token = getAuthToken(); @@ -41,10 +45,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { } 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); @@ -56,9 +56,27 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { }; useEffect(() => { + // Check auth status on mount checkAuthStatus(); }, []); + // Separate effect to handle redirect only when setup is required + useEffect(() => { + // Only redirect if setup is truly required (first time setup) + // Don't redirect if user manually navigated to /setup or is on camera page + if (!loading && requiresSetup && !authEnabled) { + const currentPath = window.location.pathname; + // Only redirect if not already on setup page or camera page, and haven't redirected yet + if (currentPath !== '/setup' && !currentPath.startsWith('/camera/') && !hasRedirectedRef.current) { + hasRedirectedRef.current = true; + window.location.href = '/setup'; + } + } else if (!requiresSetup) { + // Reset redirect flag when setup is no longer required + hasRedirectedRef.current = false; + } + }, [loading, requiresSetup, authEnabled]); + const login = async (username: string, password: string) => { const response = await api.login({ username, password }); setAuthToken(response.access_token); @@ -95,6 +113,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { value={{ user, authEnabled, + requiresSetup, loading, login, logout, diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index d9a0adec6..98e624ef8 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -2996,7 +2996,11 @@ export function SettingsPage() {