mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-05 13:41:36 +02:00
Fixed re-auth setup and gitignore node modules
This commit is contained in:
@@ -53,3 +53,6 @@ logs/
|
||||
*.log*
|
||||
bambutrack.log.*
|
||||
firmware/
|
||||
|
||||
# Node modules
|
||||
node_modules/
|
||||
@@ -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)
|
||||
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user