diff --git a/docker/nginx.conf b/docker/nginx.conf index 7afc334f..2a943a46 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -138,6 +138,15 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } + location /health { + 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 ~ ^/status(/.*)?$ { proxy_pass http://127.0.0.1:8085; proxy_http_version 1.1; diff --git a/electron-builder.json b/electron-builder.json index e0da4076..afacd22b 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -15,6 +15,9 @@ "!vite.config.ts", "!eslint.config.js" ], + "asarUnpack": [ + "node_modules/node-fetch/**/*" + ], "extraMetadata": { "main": "electron/main.cjs" }, diff --git a/electron/main.cjs b/electron/main.cjs index 677b0df1..2ef230ae 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -129,7 +129,53 @@ ipcMain.handle('save-server-config', (event, config) => { ipcMain.handle('test-server-connection', async (event, serverUrl) => { try { - const fetch = require('node-fetch'); + // Use Node.js built-in fetch (available in Node 18+) or fallback to https module + let fetch; + try { + // Try to use built-in fetch first (Node 18+) + fetch = globalThis.fetch || require('node:fetch'); + } catch (e) { + // Fallback to https module for older Node versions + const https = require('https'); + const http = require('http'); + const { URL } = require('url'); + + fetch = (url, options = {}) => { + return new Promise((resolve, reject) => { + const urlObj = new URL(url); + const isHttps = urlObj.protocol === 'https:'; + const client = isHttps ? https : http; + + const req = client.request(url, { + method: options.method || 'GET', + headers: options.headers || {}, + timeout: options.timeout || 5000 + }, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + resolve({ + ok: res.statusCode >= 200 && res.statusCode < 300, + status: res.statusCode, + text: () => Promise.resolve(data), + json: () => Promise.resolve(JSON.parse(data)) + }); + }); + }); + + req.on('error', reject); + req.on('timeout', () => { + req.destroy(); + reject(new Error('Request timeout')); + }); + + if (options.body) { + req.write(options.body); + } + req.end(); + }); + }; + } // Try multiple endpoints to test the connection const testUrls = [ diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index c8874ab7..f8c92880 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -315,7 +315,15 @@ router.get('/oidc/authorize', async (req, res) => { let origin = req.get('Origin') || req.get('Referer')?.replace(/\/[^\/]*$/, '') || 'http://localhost:5173'; - if (origin.includes('localhost')) { + // 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')) { origin = 'http://localhost:8081'; } @@ -557,7 +565,14 @@ router.get('/oidc/callback', async (req, res) => { let frontendUrl = redirectUri.replace('/users/oidc/callback', ''); - if (frontendUrl.includes('localhost')) { + // 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, redirect back to the same server URL (the frontend is served from there) + frontendUrl = redirectUri.replace('/users/oidc/callback', ''); + } else if (frontendUrl.includes('localhost')) { frontendUrl = 'http://localhost:5173'; } @@ -572,7 +587,14 @@ router.get('/oidc/callback', async (req, res) => { let frontendUrl = redirectUri.replace('/users/oidc/callback', ''); - if (frontendUrl.includes('localhost')) { + // 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, redirect back to the same server URL (the frontend is served from there) + frontendUrl = redirectUri.replace('/users/oidc/callback', ''); + } else if (frontendUrl.includes('localhost')) { frontendUrl = 'http://localhost:5173'; } diff --git a/src/ui/Desktop/Homepage/Homepage.tsx b/src/ui/Desktop/Homepage/Homepage.tsx index 3ee99521..c49bf446 100644 --- a/src/ui/Desktop/Homepage/Homepage.tsx +++ b/src/ui/Desktop/Homepage/Homepage.tsx @@ -3,7 +3,7 @@ import {HomepageAuth} from "@/ui/Desktop/Homepage/HomepageAuth.tsx"; import {HomepageUpdateLog} from "@/ui/Desktop/Homepage/HompageUpdateLog.tsx"; import {HomepageAlertManager} from "@/ui/Desktop/Homepage/HomepageAlertManager.tsx"; import {Button} from "@/components/ui/button.tsx"; -import { getUserInfo, getDatabaseHealth } from "@/ui/main-axios.ts"; +import { getUserInfo, getDatabaseHealth, setCookie, getCookie } from "@/ui/main-axios.ts"; import {useTranslation} from "react-i18next"; interface HomepageProps { @@ -11,25 +11,15 @@ interface HomepageProps { isAuthenticated: boolean; authLoading: boolean; onAuthSuccess: (authData: { isAdmin: boolean; username: string | null; userId: string | null }) => void; -} - -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=/`; + isTopbarOpen: boolean; } export function Homepage({ onSelectView, isAuthenticated, authLoading, - onAuthSuccess + onAuthSuccess, + isTopbarOpen }: HomepageProps): React.ReactElement { const {t} = useTranslation(); const [loggedIn, setLoggedIn] = useState(isAuthenticated); @@ -38,6 +28,11 @@ export function Homepage({ const [userId, setUserId] = useState(null); const [dbError, setDbError] = useState(null); + // Calculate margins based on topbar state (same logic as AppView.tsx) + const topMarginPx = isTopbarOpen ? 74 : 26; + const leftMarginPx = 26; // Assuming sidebar is collapsed for homepage + const bottomMarginPx = 8; + useEffect(() => { setLoggedIn(isAuthenticated); }, [isAuthenticated]); @@ -53,7 +48,7 @@ export function Homepage({ .then(([meRes]) => { setIsAdmin(!!meRes.is_admin); setUsername(meRes.username || null); - setUserId(meRes.userId || null); + setUserId(meRes.id || null); setDbError(null); }) .catch((err) => { @@ -72,66 +67,79 @@ export function Homepage({ return ( -
+ <> {!loggedIn ? ( - +
+ +
) : ( -
-
- +
+
+
+ -
- -
- -
- -
- +
+ +
+ +
+ +
+ +
)} -
+ ); } \ No newline at end of file diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index a6daa8c2..2aaa3545 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -102,7 +102,7 @@ export function setCookie(name: string, value: string, days = 7): void { } } -function getCookie(name: string): string | undefined { +export function getCookie(name: string): string | undefined { const isElectron = (window as any).IS_ELECTRON === true || (window as any).electronAPI?.isElectron === true; if (isElectron) { const token = localStorage.getItem(name) || undefined; @@ -157,6 +157,12 @@ function createApiInstance(baseURL: string, serviceName: string = 'API'): AxiosI authLogger.warn('No JWT token found, request will be unauthenticated', context); } + // Add Electron-specific headers for OIDC and other backend detection + if (isElectron) { + config.headers['X-Electron-App'] = 'true'; + config.headers['User-Agent'] = 'Termix-Electron/1.6.0'; + } + return config; });