v1.6.0 #221

Merged
LukeGus merged 74 commits from dev-1.6.0 into main 2025-09-12 19:42:00 +00:00
5 changed files with 202 additions and 39 deletions
Showing only changes of commit aa8738469f - Show all commits

View File

@@ -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 };
}

View File

@@ -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

View File

@@ -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) => {

View File

@@ -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>
)}

View File

@@ -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 {