Fixed re-auth setup and gitignore node modules

This commit is contained in:
JesseFPV
2026-01-22 12:21:07 +01:00
parent 821d3ba748
commit 3a848643c0
8 changed files with 586 additions and 494 deletions
+3
View File
@@ -53,3 +53,6 @@ logs/
*.log*
bambutrack.log.*
firmware/
# Node modules
node_modules/
+69 -33
View File
@@ -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)
+3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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>