Files
ProxmoxVE-Local/src/app/_components/AuthProvider.tsx
Michel Roegl-Brunner 6265ffeab5 feat: Implement comprehensive authentication system (#99)
* feat: implement JWT-based authentication system

- Add bcrypt password hashing and JWT token generation
- Create blocking auth modals for login and setup
- Add authentication management to General Settings
- Implement API routes for login, verify, setup, and credential management
- Add AuthProvider and AuthGuard components
- Support first-time setup and persistent authentication
- Store credentials securely in .env file

* feat: add option to skip enabling auth during setup

- Add toggle in SetupModal to choose whether to enable authentication immediately
- Users can set up credentials but keep authentication disabled initially
- Authentication can be enabled/disabled later through General Settings
- Maintains flexibility for users who want to configure auth gradually

* fix: allow proceeding without password when auth is disabled

- Make password fields optional when authentication is disabled in setup
- Update button validation to only require password when auth is enabled
- Modify API to handle optional password parameter
- Update hasCredentials logic to work with username-only setup
- Users can now complete setup with just username when auth is disabled
- Password can be added later when enabling authentication

* feat: don't store credentials when authentication is disabled

- When auth is disabled, no username or password is stored
- Setup modal only requires credentials when authentication is enabled
- Disabling authentication clears all stored credentials
- Users can skip authentication entirely without storing any data
- Clean separation between enabled/disabled authentication states

* feat: add setup completed flag to prevent modal on every load

- Add AUTH_SETUP_COMPLETED flag to track when user has completed setup
- Setup modal only appears when setupCompleted is false
- Both enabled and disabled auth setups mark setup as completed
- Clean .env file when authentication is disabled (no empty credential lines)
- Prevents setup modal from appearing on every page load after user decision

* fix: add missing Authentication tab button in settings modal

- Authentication tab button was missing from the tabs navigation
- Users couldn't access authentication settings
- Added Authentication tab button with proper styling and click handler
- Authentication settings are now accessible through the settings modal

* fix: properly load and display authentication settings

- Add setupCompleted state variable to track setup status
- Update loadAuthCredentials to include setupCompleted field
- Fix authentication status display logic to show correct state
- Show proper status when auth is disabled but setup is completed
- Enable toggle only when setup is completed (not just when credentials exist)
- Settings now correctly reflect the actual authentication state

* fix: handle empty FILTERS environment variable

- Add check for empty or invalid FILTERS JSON before parsing
- Prevents 'Unexpected end of JSON input' error when FILTERS is empty
- Return null filters instead of throwing parse error
- Clean up empty FILTERS line from .env file
- Fixes console error when loading settings modal

* fix: load authentication credentials when settings modal opens

- Add loadAuthCredentials() call to useEffect when modal opens
- Authentication settings were not loading because the function wasn't being called
- Now properly loads auth configuration when settings modal is opened
- Settings will display the correct authentication status and state

* fix: prevent multiple JWT secret generation with caching

- Add JWT secret caching to prevent race conditions
- Multiple API calls were generating duplicate JWT secrets
- Now caches secret after first generation/read
- Clean up duplicate JWT_SECRET lines from .env file
- Prevents .env file from being cluttered with multiple secrets

* feat: auto-login user after setup with authentication enabled

- When user sets up authentication with credentials, automatically log them in
- Prevents need to manually log in after setup completion
- Setup modal now calls login API after successful setup when auth is enabled
- AuthGuard no longer reloads page after setup, just refreshes config
- Seamless user experience from setup to authenticated state

* fix: resolve console errors and improve auth flow

- Fix 401 Unauthorized error by checking setup status before auth verification
- AuthProvider now checks if setup is completed before attempting to verify auth
- Prevents unnecessary auth verification calls when no credentials exist
- Add webpack polling configuration to fix WebSocket HMR issues
- Improves development experience when accessing from different IPs
- Eliminates console errors during initial setup flow

* fix: resolve build errors and linting issues

- Fix TypeScript ESLint error: use optional chain expression in auth.ts
- Fix React Hook warning: add missing 'isRunning' dependency to useEffect in Terminal.tsx
- Build now compiles successfully without any errors or warnings
- All linting rules are now satisfied
2025-10-10 12:45:45 +02:00

120 lines
3.2 KiB
TypeScript

'use client';
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
interface AuthContextType {
isAuthenticated: boolean;
username: string | null;
isLoading: boolean;
login: (username: string, password: string) => Promise<boolean>;
logout: () => void;
checkAuth: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
interface AuthProviderProps {
children: ReactNode;
}
export function AuthProvider({ children }: AuthProviderProps) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [username, setUsername] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const checkAuth = async () => {
try {
// First check if setup is completed
const setupResponse = await fetch('/api/settings/auth-credentials');
if (setupResponse.ok) {
const setupData = await setupResponse.json() as { setupCompleted: boolean; enabled: boolean };
// If setup is not completed or auth is disabled, don't verify
if (!setupData.setupCompleted || !setupData.enabled) {
setIsAuthenticated(false);
setUsername(null);
setIsLoading(false);
return;
}
}
// Only verify authentication if setup is completed and auth is enabled
const response = await fetch('/api/auth/verify');
if (response.ok) {
const data = await response.json() as { username: string };
setIsAuthenticated(true);
setUsername(data.username);
} else {
setIsAuthenticated(false);
setUsername(null);
}
} catch (error) {
console.error('Error checking auth:', error);
setIsAuthenticated(false);
setUsername(null);
} finally {
setIsLoading(false);
}
};
const login = async (username: string, password: string): Promise<boolean> => {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
});
if (response.ok) {
const data = await response.json() as { username: string };
setIsAuthenticated(true);
setUsername(data.username);
return true;
} else {
const errorData = await response.json();
console.error('Login failed:', errorData.error);
return false;
}
} catch (error) {
console.error('Login error:', error);
return false;
}
};
const logout = () => {
// Clear the auth cookie by setting it to expire
document.cookie = 'auth-token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
setIsAuthenticated(false);
setUsername(null);
};
useEffect(() => {
void checkAuth();
}, []);
return (
<AuthContext.Provider
value={{
isAuthenticated,
username,
isLoading,
login,
logout,
checkAuth,
}}
>
{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;
}