Explorar el Código

Merge pull request #128 from JesseFPV/0.1.6b11

Fixed re-auth setup and gitignore node modules
MartinNYHC hace 7 meses
padre
commit
ce8968f893

+ 3 - 0
.gitignore

@@ -53,3 +53,6 @@ logs/
 *.log*
 bambutrack.log.*
 firmware/
+
+# Node modules
+node_modules/

+ 72 - 36
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 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
+            # 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
+
+            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)",
+                    )
+
+                # 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",
+                    )
+
+                # 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)

+ 3 - 0
frontend/src/App.tsx

@@ -82,6 +82,9 @@ function SetupRoute({ children }: { children: React.ReactNode }) {
     return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
   }
 
+  // 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 <Navigate to="/login" replace />;
   }

+ 24 - 5
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<void>;
   logout: () => void;
@@ -17,12 +18,15 @@ 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 [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,

+ 5 - 1
frontend/src/pages/SettingsPage.tsx

@@ -2996,7 +2996,11 @@ export function SettingsPage() {
                       </ul>
 
                       <Button
-                        onClick={() => navigate('/setup')}
+                        type="button"
+                        onClick={(e) => {
+                          e.preventDefault();
+                          navigate('/setup');
+                        }}
                         className="w-full"
                       >
                         <Lock className="w-4 h-4" />

+ 64 - 37
frontend/src/pages/SetupPage.tsx

@@ -4,11 +4,14 @@ import { useMutation } from '@tanstack/react-query';
 import { api } from '../api/client';
 import { useToast } from '../contexts/ToastContext';
 import { useTheme } from '../contexts/ThemeContext';
+import { useAuth } from '../contexts/AuthContext';
+import { Info } from 'lucide-react';
 
 export function SetupPage() {
   const navigate = useNavigate();
   const { showToast } = useToast();
   const { mode } = useTheme();
+  const { refreshAuth } = useAuth();
   const [authEnabled, setAuthEnabled] = useState(false);
   const [adminUsername, setAdminUsername] = useState('');
   const [adminPassword, setAdminPassword] = useState('');
@@ -21,10 +24,18 @@ export function SetupPage() {
         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');
+    onSuccess: async (data) => {
+      // Refresh auth status after setup
+      await refreshAuth();
+      
+      if (data.auth_enabled) {
+        if (data.admin_created) {
+          showToast('Authentication enabled and admin user created');
+          navigate('/login');
+        } else {
+          showToast('Authentication enabled using existing admin users');
+          navigate('/login');
+        }
       } else {
         showToast('Setup completed');
         navigate('/');
@@ -39,17 +50,21 @@ export function SetupPage() {
     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;
+      // Only validate if credentials are provided
+      // If no credentials provided, backend will use existing admin users if they exist
+      if (adminUsername || adminPassword) {
+        if (!adminUsername || !adminPassword) {
+          showToast('Please enter both admin username and password, or leave both empty to use existing admin users', '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;
+        }
       }
     }
 
@@ -92,55 +107,67 @@ export function SetupPage() {
 
             {authEnabled && (
               <div className="space-y-4 mt-4">
+                <div className="p-3 bg-bambu-dark-secondary/50 border border-bambu-dark-tertiary rounded-lg">
+                  <div className="flex items-start gap-2">
+                    <Info className="w-4 h-4 text-bambu-green mt-0.5 flex-shrink-0" />
+                    <div className="text-sm text-bambu-gray">
+                      <p className="text-white font-medium mb-1">Admin Account</p>
+                      <p>
+                        If admin users already exist, authentication will be enabled using the existing admin accounts.
+                        Leave the fields below empty to use existing admins, or enter new credentials to create a new admin user.
+                      </p>
+                    </div>
+                  </div>
+                </div>
+
                 <div>
                   <label htmlFor="admin-username" className="block text-sm font-medium text-white mb-2">
-                    Admin Username
+                    Admin Username <span className="text-bambu-gray text-xs">(optional if admin users exist)</span>
                   </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"
+                    placeholder="Enter admin username (optional)"
                     autoComplete="username"
                   />
                 </div>
 
                 <div>
                   <label htmlFor="admin-password" className="block text-sm font-medium text-white mb-2">
-                    Admin Password
+                    Admin Password <span className="text-bambu-gray text-xs">(optional if admin users exist)</span>
                   </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"
+                    placeholder="Enter admin password (optional)"
                     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>
+                {adminPassword && (
+                  <div>
+                    <label htmlFor="confirm-password" className="block text-sm font-medium text-white mb-2">
+                      Confirm Password
+                    </label>
+                    <input
+                      id="confirm-password"
+                      type="password"
+                      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>

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/index-7nHE42SH.js


+ 1 - 1
static/index.html

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

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio