diff --git a/electron/main.cjs b/electron/main.cjs index 2ef230ae..1dfe8b24 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -127,6 +127,19 @@ ipcMain.handle('save-server-config', (event, config) => { } }); +// OIDC success/error handlers +ipcMain.handle('oidc-success', (event, data) => { + console.log('OIDC authentication successful:', data); + // You can add additional logic here if needed + return { success: true }; +}); + +ipcMain.handle('oidc-error', (event, data) => { + console.log('OIDC authentication error:', data); + // You can add additional logic here if needed + return { success: false, error: data.error }; +}); + ipcMain.handle('test-server-connection', async (event, serverUrl) => { try { // Use Node.js built-in fetch (available in Node 18+) or fallback to https module @@ -177,31 +190,47 @@ ipcMain.handle('test-server-connection', async (event, serverUrl) => { }; } - // Try multiple endpoints to test the connection - const testUrls = [ - `${serverUrl}/health`, - `${serverUrl}/version`, - `${serverUrl}/users/registration-allowed` - ]; + // Test the health endpoint specifically - this is required for a valid Termix server + const healthUrl = `${serverUrl}/health`; - for (const testUrl of testUrls) { - try { - const response = await fetch(testUrl, { - method: 'GET', - timeout: 5000 - }); - - if (response.ok) { - // If we get a 200 response, it's likely a valid Termix server - return { success: true, status: response.status, testedUrl: testUrl }; + try { + const response = await fetch(healthUrl, { + method: 'GET', + timeout: 5000 + }); + + if (response.ok) { + // Try to parse the response to ensure it's a valid health check + const data = await response.text(); + // A valid health check should return some JSON or text indicating the server is healthy + if (data && (data.includes('healthy') || data.includes('ok') || data.includes('status') || response.status === 200)) { + return { success: true, status: response.status, testedUrl: healthUrl }; } - } catch (urlError) { - // Continue to next URL if this one fails - continue; } + } catch (urlError) { + console.error('Health check failed:', urlError); } - return { success: false, error: 'Server is not responding or not a valid Termix server' }; + // If health check fails, try version endpoint as fallback + try { + const versionUrl = `${serverUrl}/version`; + const response = await fetch(versionUrl, { + method: 'GET', + timeout: 5000 + }); + + if (response.ok) { + const data = await response.text(); + // Check if it looks like a Termix version response + if (data && (data.includes('version') || data.includes('termix') || data.includes('1.') || response.status === 200)) { + return { success: true, status: response.status, testedUrl: versionUrl, warning: 'Health endpoint not available, but server appears to be running' }; + } + } + } catch (versionError) { + console.error('Version check failed:', versionError); + } + + return { success: false, error: 'Server is not responding or does not appear to be a valid Termix server. Please ensure the server is running and accessible.' }; } catch (error) { return { success: false, error: error.message }; } diff --git a/electron/preload.js b/electron/preload.js index 8ffd70d1..9ecd3300 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -28,7 +28,11 @@ contextBridge.exposeInMainWorld('electronAPI', { isDev: process.env.NODE_ENV === 'development', // Generic invoke method - invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args) + invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args), + + // OIDC handlers + oidcSuccess: (data) => ipcRenderer.invoke('oidc-success', data), + oidcError: (data) => ipcRenderer.invoke('oidc-error', data) }); // Also set the legacy IS_ELECTRON flag for backward compatibility diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index f8c92880..fcd5b342 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -570,8 +570,9 @@ router.get('/oidc/callback', async (req, res) => { const isElectron = userAgent.includes('Electron') || req.get('X-Electron-App') === 'true'; if (isElectron) { - // For Electron, redirect back to the same server URL (the frontend is served from there) - frontendUrl = redirectUri.replace('/users/oidc/callback', ''); + // For Electron, we need to redirect to a special endpoint that will handle the token + // and then redirect to the Electron app using a custom protocol or file URL + frontendUrl = redirectUri.replace('/users/oidc/callback', '/electron-oidc-success'); } else if (frontendUrl.includes('localhost')) { frontendUrl = 'http://localhost:5173'; } @@ -592,8 +593,8 @@ router.get('/oidc/callback', async (req, res) => { const isElectron = userAgent.includes('Electron') || req.get('X-Electron-App') === 'true'; if (isElectron) { - // For Electron, redirect back to the same server URL (the frontend is served from there) - frontendUrl = redirectUri.replace('/users/oidc/callback', ''); + // For Electron, we need to redirect to a special endpoint that will handle the error + frontendUrl = redirectUri.replace('/users/oidc/callback', '/electron-oidc-error'); } else if (frontendUrl.includes('localhost')) { frontendUrl = 'http://localhost:5173'; } @@ -605,6 +606,140 @@ router.get('/oidc/callback', async (req, res) => { } }); +// Route: Electron OIDC success handler +// GET /electron-oidc-success +router.get('/electron-oidc-success', (req, res) => { + const { success, token, error } = req.query; + + if (success === 'true' && token) { + // Return an HTML page that will communicate with the Electron app + res.send(` + + + + OIDC Authentication Success + + + +
+

Authentication Successful!

+

You can close this window and return to the Termix application.

+
+ + + + `); + } else if (error) { + res.send(` + + + + OIDC Authentication Error + + + +
+

Authentication Failed

+

Error: ${error}

+

You can close this window and try again.

+
+ + + + `); + } else { + res.status(400).send('Invalid request'); + } +}); + +// Route: Electron OIDC error handler +// GET /electron-oidc-error +router.get('/electron-oidc-error', (req, res) => { + const { error } = req.query; + + res.send(` + + + + OIDC Authentication Error + + + +
+

Authentication Failed

+

Error: ${error || 'Unknown error'}

+

You can close this window and try again.

+
+ + + + `); +}); + // Route: Get user JWT by username and password (traditional login) // POST /users/login router.post('/login', async (req, res) => { diff --git a/src/ui/Desktop/DesktopApp.tsx b/src/ui/Desktop/DesktopApp.tsx index 8f4ef088..60eb5481 100644 --- a/src/ui/Desktop/DesktopApp.tsx +++ b/src/ui/Desktop/DesktopApp.tsx @@ -8,19 +8,7 @@ import {TopNavbar} from "@/ui/Desktop/Navigation/TopNavbar.tsx"; import { AdminSettings } from "@/ui/Desktop/Admin/AdminSettings.tsx"; import { UserProfile } from "@/ui/Desktop/User/UserProfile.tsx"; import { Toaster } from "@/components/ui/sonner.tsx"; -import { getUserInfo } from "@/ui/main-axios.ts"; - -function getCookie(name: string) { - return document.cookie.split('; ').reduce((r, v) => { - const parts = v.split('='); - return parts[0] === name ? decodeURIComponent(parts[1]) : r; - }, ""); -} - -function setCookie(name: string, value: string, days = 7) { - const expires = new Date(Date.now() + days * 864e5).toUTCString(); - document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/`; -} +import { getUserInfo, getCookie, setCookie } from "@/ui/main-axios.ts"; function AppContent() { const [view, setView] = useState("homepage") @@ -115,6 +103,7 @@ function AppContent() { isAuthenticated={isAuthenticated} authLoading={authLoading} onAuthSuccess={handleAuthSuccess} + isTopbarOpen={isTopbarOpen} /> )} diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index 2aaa3545..0b0adecd 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -341,7 +341,8 @@ function getApiUrl(path: string, defaultPort: number): string { const baseUrl = configuredServerUrl.replace(/\/$/, ''); return `${baseUrl}${path}`; } - return `http://127.0.0.1:${defaultPort}${path}`; + // In Electron without configured server, return a placeholder that will cause requests to fail gracefully + return 'http://no-server-configured'; } else if (isDev) { return `http://${apiHost}:${defaultPort}${path}`; } else { @@ -470,6 +471,11 @@ function handleApiError(error: unknown, operation: string): never { apiLogger.error(`Server error: ${method} ${url} - ${message}`, error, errorContext); throw new ApiError('Server error occurred. Please try again later.', status, 'SERVER_ERROR'); } else if (status === 0) { + // Check if this is a "no server configured" error + if (url.includes('no-server-configured')) { + apiLogger.error(`No server configured: ${method} ${url}`, error, errorContext); + throw new ApiError('No server configured. Please configure a Termix server first.', 0, 'NO_SERVER_CONFIGURED'); + } apiLogger.error(`Network error: ${method} ${url} - ${message}`, error, errorContext); throw new ApiError('Network error. Please check your connection and try again.', 0, 'NETWORK_ERROR'); } else {