v1.6.0 #221
@@ -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;
|
||||
|
||||
@@ -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('<html') || data.includes('<!DOCTYPE') || data.includes('<head>') || data.includes('<body>')) {
|
||||
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('<html') || data.includes('<!DOCTYPE') || data.includes('<head>') || data.includes('<body>')) {
|
||||
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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>OIDC Authentication Success</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
text-align: center;
|
||||
padding: 50px;
|
||||
background: #1a1a1a;
|
||||
color: white;
|
||||
}
|
||||
.success { color: #4ade80; }
|
||||
.error { color: #f87171; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="success">
|
||||
<h2>Authentication Successful!</h2>
|
||||
<p>You can close this window and return to the Termix application.</p>
|
||||
</div>
|
||||
<script>
|
||||
// Try to communicate with the Electron app
|
||||
if (window.electronAPI) {
|
||||
window.electronAPI.invoke('oidc-success', { token: '${token}' });
|
||||
}
|
||||
|
||||
// Fallback: try to close the window after a delay
|
||||
setTimeout(() => {
|
||||
if (window.close) {
|
||||
window.close();
|
||||
}
|
||||
}, 2000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
} else if (error) {
|
||||
res.send(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>OIDC Authentication Error</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
text-align: center;
|
||||
padding: 50px;
|
||||
background: #1a1a1a;
|
||||
color: white;
|
||||
}
|
||||
.error { color: #f87171; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error">
|
||||
<h2>Authentication Failed</h2>
|
||||
<p>Error: ${error}</p>
|
||||
<p>You can close this window and try again.</p>
|
||||
</div>
|
||||
<script>
|
||||
// Try to communicate with the Electron app
|
||||
if (window.electronAPI) {
|
||||
window.electronAPI.invoke('oidc-error', { error: '${error}' });
|
||||
}
|
||||
|
||||
// Fallback: try to close the window after a delay
|
||||
setTimeout(() => {
|
||||
if (window.close) {
|
||||
window.close();
|
||||
}
|
||||
}, 3000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
} 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(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>OIDC Authentication Error</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
text-align: center;
|
||||
padding: 50px;
|
||||
background: #1a1a1a;
|
||||
color: white;
|
||||
}
|
||||
.error { color: #f87171; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error">
|
||||
<h2>Authentication Failed</h2>
|
||||
<p>Error: ${error || 'Unknown error'}</p>
|
||||
<p>You can close this window and try again.</p>
|
||||
</div>
|
||||
<script>
|
||||
// Try to communicate with the Electron app
|
||||
if (window.electronAPI) {
|
||||
window.electronAPI.invoke('oidc-error', { error: '${error || 'Unknown error'}' });
|
||||
}
|
||||
|
||||
// Fallback: try to close the window after a delay
|
||||
setTimeout(() => {
|
||||
if (window.close) {
|
||||
window.close();
|
||||
}
|
||||
}, 3000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
});
|
||||
|
||||
// Route: Get user JWT by username and password (traditional login)
|
||||
// POST /users/login
|
||||
router.post('/login', async (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user