diff --git a/docker/nginx.conf b/docker/nginx.conf index 85747ea2..2a943a46 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -63,25 +63,6 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } - # Electron OIDC success/error handlers - location /electron-oidc-success { - proxy_pass http://127.0.0.1:8081; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - location /electron-oidc-error { - proxy_pass http://127.0.0.1:8081; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - location /ssh/ { proxy_pass http://127.0.0.1:8081; proxy_http_version 1.1; diff --git a/electron/main.cjs b/electron/main.cjs index 913abb45..5fb0b348 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -127,18 +127,6 @@ 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 { @@ -204,18 +192,28 @@ ipcMain.handle('test-server-connection', async (event, serverUrl) => { if (response.ok) { const data = await response.text(); + + // Reject if response looks like HTML (YouTube, etc.) + if (data.includes('') || data.includes('')) { + console.log('Health endpoint returned HTML instead of JSON - not a Termix server'); + return { success: false, error: 'Server returned HTML instead of JSON. This does not appear to be a Termix server.' }; + } + // A valid Termix health check should return JSON with specific structure try { const healthData = JSON.parse(data); - // Check if it has the expected health check structure - if (healthData && (healthData.status === 'healthy' || healthData.healthy === true || healthData.database === 'connected')) { + // Check if it has the expected Termix health check structure + if (healthData && ( + healthData.status === 'healthy' || + healthData.healthy === true || + healthData.database === 'connected' || + (healthData.app && healthData.app.toLowerCase().includes('termix')) + )) { return { success: true, status: response.status, testedUrl: healthUrl }; } } catch (parseError) { - // If not JSON, check for text indicators - if (data && (data.includes('healthy') || data.includes('ok') || data.includes('connected'))) { - return { success: true, status: response.status, testedUrl: healthUrl }; - } + // If not JSON, reject - Termix health endpoint should return JSON + console.log('Health endpoint did not return valid JSON'); } } } catch (urlError) { @@ -232,17 +230,26 @@ ipcMain.handle('test-server-connection', async (event, serverUrl) => { if (response.ok) { const data = await response.text(); + + // Reject if response looks like HTML (YouTube, etc.) + if (data.includes('') || data.includes('')) { + console.log('Version endpoint returned HTML instead of JSON - not a Termix server'); + return { success: false, error: 'Server returned HTML instead of JSON. This does not appear to be a Termix server.' }; + } + try { const versionData = JSON.parse(data); - // Check if it looks like a Termix version response - if (versionData && (versionData.version || versionData.app === 'termix' || versionData.name === 'termix')) { + // Check if it looks like a Termix version response - must be JSON and contain Termix-specific fields + if (versionData && ( + (versionData.app && versionData.app.toLowerCase().includes('termix')) || + (versionData.name && versionData.name.toLowerCase().includes('termix')) || + (versionData.version && versionData.description && versionData.description.toLowerCase().includes('termix')) + )) { return { success: true, status: response.status, testedUrl: versionUrl, warning: 'Health endpoint not available, but server appears to be running' }; } } catch (parseError) { - // If not JSON, check for text indicators - if (data && (data.includes('termix') || data.includes('1.6.0') || data.includes('version'))) { - return { success: true, status: response.status, testedUrl: versionUrl, warning: 'Health endpoint not available, but server appears to be running' }; - } + // If not JSON, reject - Termix version endpoint should return JSON + console.log('Version endpoint did not return valid JSON'); } } } catch (versionError) { diff --git a/electron/preload.js b/electron/preload.js index 9ecd3300..9ca10696 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -30,9 +30,6 @@ contextBridge.exposeInMainWorld('electronAPI', { // Generic invoke method 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 fcd5b342..07fecfa2 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -135,9 +135,9 @@ router.post('/create', async (req, res) => { if (row && (row as any).value !== 'true') { return res.status(403).json({error: 'Registration is currently disabled'}); } - } catch (e) { - authLogger.warn('Failed to check registration status', { operation: 'registration_check', error: e }); - } + } catch (e) { + authLogger.warn('Failed to check registration status', { operation: 'registration_check', error: e }); + } const {username, password} = req.body; @@ -216,21 +216,21 @@ router.post('/oidc-config', authenticateJWT, async (req, res) => { name_path, scopes } = req.body; - - const isDisableRequest = (client_id === '' || client_id === null || client_id === undefined) && - (client_secret === '' || client_secret === null || client_secret === undefined) && - (issuer_url === '' || issuer_url === null || issuer_url === undefined) && - (authorization_url === '' || authorization_url === null || authorization_url === undefined) && - (token_url === '' || token_url === null || token_url === undefined); - + + const isDisableRequest = (client_id === '' || client_id === null || client_id === undefined) && + (client_secret === '' || client_secret === null || client_secret === undefined) && + (issuer_url === '' || issuer_url === null || issuer_url === undefined) && + (authorization_url === '' || authorization_url === null || authorization_url === undefined) && + (token_url === '' || token_url === null || token_url === undefined); + const isEnableRequest = isNonEmptyString(client_id) && isNonEmptyString(client_secret) && - isNonEmptyString(issuer_url) && isNonEmptyString(authorization_url) && - isNonEmptyString(token_url) && isNonEmptyString(identifier_path) && - isNonEmptyString(name_path); + isNonEmptyString(issuer_url) && isNonEmptyString(authorization_url) && + isNonEmptyString(token_url) && isNonEmptyString(identifier_path) && + isNonEmptyString(name_path); if (!isDisableRequest && !isEnableRequest) { - authLogger.warn('OIDC validation failed - neither disable nor enable request', { - operation: 'oidc_config_update', + authLogger.warn('OIDC validation failed - neither disable nor enable request', { + operation: 'oidc_config_update', userId, isDisableRequest, isEnableRequest @@ -275,7 +275,7 @@ router.delete('/oidc-config', authenticateJWT, async (req, res) => { if (!user || user.length === 0 || !user[0].is_admin) { return res.status(403).json({error: 'Not authorized'}); } - + db.$client.prepare("DELETE FROM settings WHERE key = 'oidc_config'").run(); authLogger.success('OIDC configuration disabled', { operation: 'oidc_disable', userId }); res.json({message: 'OIDC configuration disabled'}); @@ -315,15 +315,7 @@ router.get('/oidc/authorize', async (req, res) => { let origin = req.get('Origin') || req.get('Referer')?.replace(/\/[^\/]*$/, '') || 'http://localhost:5173'; - // Handle Electron app - check for custom headers or user agent - const userAgent = req.get('User-Agent') || ''; - const isElectron = userAgent.includes('Electron') || req.get('X-Electron-App') === 'true'; - - if (isElectron) { - // For Electron, use the configured server URL or fallback to localhost - const serverUrl = process.env.SERVER_URL || 'http://localhost:8081'; - origin = serverUrl; - } else if (origin.includes('localhost')) { + if (origin.includes('localhost')) { origin = 'http://localhost:8081'; } @@ -541,7 +533,7 @@ router.get('/oidc/callback', async (req, res) => { .select() .from(users) .where(eq(users.id, id)); - + // OIDC user created - toast notification handled by frontend } else { await db.update(users) @@ -552,7 +544,7 @@ router.get('/oidc/callback', async (req, res) => { .select() .from(users) .where(eq(users.id, user[0].id)); - + // OIDC user logged in - toast notification handled by frontend } @@ -565,15 +557,7 @@ router.get('/oidc/callback', async (req, res) => { let frontendUrl = redirectUri.replace('/users/oidc/callback', ''); - // Handle Electron app redirects - const userAgent = req.get('User-Agent') || ''; - const isElectron = userAgent.includes('Electron') || req.get('X-Electron-App') === 'true'; - - if (isElectron) { - // 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')) { + if (frontendUrl.includes('localhost')) { frontendUrl = 'http://localhost:5173'; } @@ -588,14 +572,7 @@ router.get('/oidc/callback', async (req, res) => { let frontendUrl = redirectUri.replace('/users/oidc/callback', ''); - // Handle Electron app redirects - const userAgent = req.get('User-Agent') || ''; - const isElectron = userAgent.includes('Electron') || req.get('X-Electron-App') === 'true'; - - if (isElectron) { - // 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')) { + if (frontendUrl.includes('localhost')) { frontendUrl = 'http://localhost:5173'; } @@ -606,140 +583,6 @@ 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) => {