v1.6.0 #221
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(`
|
||||
<!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) => {
|
||||
|
||||
@@ -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<string>("homepage")
|
||||
@@ -115,6 +103,7 @@ function AppContent() {
|
||||
isAuthenticated={isAuthenticated}
|
||||
authLoading={authLoading}
|
||||
onAuthSuccess={handleAuthSuccess}
|
||||
isTopbarOpen={isTopbarOpen}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user