diff --git a/electron/main.cjs b/electron/main.cjs index 1dfe8b24..913abb45 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -190,8 +190,11 @@ ipcMain.handle('test-server-connection', async (event, serverUrl) => { }; } + // Normalize the server URL (remove trailing slash) + const normalizedServerUrl = serverUrl.replace(/\/$/, ''); + // Test the health endpoint specifically - this is required for a valid Termix server - const healthUrl = `${serverUrl}/health`; + const healthUrl = `${normalizedServerUrl}/health`; try { const response = await fetch(healthUrl, { @@ -200,11 +203,19 @@ ipcMain.handle('test-server-connection', async (event, serverUrl) => { }); 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 }; + // 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')) { + 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 }; + } } } } catch (urlError) { @@ -213,7 +224,7 @@ ipcMain.handle('test-server-connection', async (event, serverUrl) => { // If health check fails, try version endpoint as fallback try { - const versionUrl = `${serverUrl}/version`; + const versionUrl = `${normalizedServerUrl}/version`; const response = await fetch(versionUrl, { method: 'GET', timeout: 5000 @@ -221,9 +232,17 @@ ipcMain.handle('test-server-connection', async (event, serverUrl) => { 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' }; + 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')) { + 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' }; + } } } } catch (versionError) { diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index a4a224d5..40c40d9d 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -787,6 +787,7 @@ "external": "External", "loginWithExternal": "Login with External Provider", "loginWithExternalDesc": "Login using your configured external identity provider", + "externalNotSupportedInElectron": "External authentication is not supported in the Electron app yet. Please use the web version for OIDC login.", "resetPasswordButton": "Reset Password", "sendResetCode": "Send Reset Code", "resetCodeDesc": "Enter your username to receive a password reset code. The code will be logged in the docker container logs.", diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index e500d3f2..6b3c3e08 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -770,6 +770,7 @@ "external": "外部", "loginWithExternal": "使用外部提供商登录", "loginWithExternalDesc": "使用您配置的外部身份提供者登录", + "externalNotSupportedInElectron": "Electron 应用暂不支持外部身份验证。请使用网页版本进行 OIDC 登录。", "resetPasswordButton": "重置密码", "sendResetCode": "发送重置代码", "resetCodeDesc": "输入您的用户名以接收密码重置代码。代码将记录在 docker 容器日志中。", diff --git a/src/ui/Desktop/Admin/AdminSettings.tsx b/src/ui/Desktop/Admin/AdminSettings.tsx index 34b4689a..c0fe69bc 100644 --- a/src/ui/Desktop/Admin/AdminSettings.tsx +++ b/src/ui/Desktop/Admin/AdminSettings.tsx @@ -79,18 +79,43 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React. React.useEffect(() => { const jwt = getCookie("jwt"); if (!jwt) return; + + // Check if we're in Electron and have a server configured + const isElectron = (window as any).IS_ELECTRON === true || (window as any).electronAPI?.isElectron === true; + if (isElectron) { + // In Electron, check if we have a configured server + const serverUrl = (window as any).configuredServerUrl; + if (!serverUrl) { + console.log('No server configured in Electron, skipping API calls'); + return; + } + } + getOIDCConfig() .then(res => { if (res) setOidcConfig(res); }) .catch((err) => { console.error('Failed to fetch OIDC config:', err); - toast.error(t('admin.failedToFetchOidcConfig')); + // Only show error if it's not a "no server configured" error + if (!err.message?.includes('No server configured')) { + toast.error(t('admin.failedToFetchOidcConfig')); + } }); fetchUsers(); }, []); React.useEffect(() => { + // Check if we're in Electron and have a server configured + const isElectron = (window as any).IS_ELECTRON === true || (window as any).electronAPI?.isElectron === true; + if (isElectron) { + const serverUrl = (window as any).configuredServerUrl; + if (!serverUrl) { + console.log('No server configured in Electron, skipping registration status check'); + return; + } + } + getRegistrationAllowed() .then(res => { if (typeof res?.allowed === 'boolean') { @@ -99,17 +124,37 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React. }) .catch((err) => { console.error('Failed to fetch registration status:', err); - toast.error(t('admin.failedToFetchRegistrationStatus')); + // Only show error if it's not a "no server configured" error + if (!err.message?.includes('No server configured')) { + toast.error(t('admin.failedToFetchRegistrationStatus')); + } }); }, []); const fetchUsers = async () => { const jwt = getCookie("jwt"); if (!jwt) return; + + // Check if we're in Electron and have a server configured + const isElectron = (window as any).IS_ELECTRON === true || (window as any).electronAPI?.isElectron === true; + if (isElectron) { + const serverUrl = (window as any).configuredServerUrl; + if (!serverUrl) { + console.log('No server configured in Electron, skipping user fetch'); + return; + } + } + setUsersLoading(true); try { const response = await getUserList(); setUsers(response.users); + } catch (err) { + console.error('Failed to fetch users:', err); + // Only show error if it's not a "no server configured" error + if (!err.message?.includes('No server configured')) { + toast.error(t('admin.failedToFetchUsers')); + } } finally { setUsersLoading(false); } diff --git a/src/ui/Desktop/Apps/Terminal/Terminal.tsx b/src/ui/Desktop/Apps/Terminal/Terminal.tsx index 323a3c7b..cd4e2362 100644 --- a/src/ui/Desktop/Apps/Terminal/Terminal.tsx +++ b/src/ui/Desktop/Apps/Terminal/Terminal.tsx @@ -212,7 +212,7 @@ export const Terminal = forwardRef(function SSHTerminal( const baseUrl = (window as any).configuredServerUrl || 'http://127.0.0.1:8081'; // Convert HTTP/HTTPS to WS/WSS and use nginx reverse proxy path const wsProtocol = baseUrl.startsWith('https://') ? 'wss://' : 'ws://'; - const wsHost = baseUrl.replace(/^https?:\/\//, '').replace(/:\d+$/, ''); // Remove port if present + const wsHost = baseUrl.replace(/^https?:\/\//, ''); // Keep the port return `${wsProtocol}${wsHost}/ssh/websocket/`; })() : `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ssh/websocket/`; diff --git a/src/ui/Desktop/Homepage/HomepageAuth.tsx b/src/ui/Desktop/Homepage/HomepageAuth.tsx index 304c1392..f60bbf6f 100644 --- a/src/ui/Desktop/Homepage/HomepageAuth.tsx +++ b/src/ui/Desktop/Homepage/HomepageAuth.tsx @@ -185,12 +185,12 @@ export function HomepageAuth({ setLoggedIn(true); setIsAdmin(!!meRes.is_admin); setUsername(meRes.username || null); - setUserId(meRes.userId || null); + setUserId(meRes.id || null); setDbError(null); onAuthSuccess({ isAdmin: !!meRes.is_admin, username: meRes.username || null, - userId: meRes.userId || null + userId: meRes.id || null }); setInternalLoggedIn(true); if (tab === "signup") { @@ -320,12 +320,12 @@ export function HomepageAuth({ setLoggedIn(true); setIsAdmin(!!meRes.is_admin); setUsername(meRes.username || null); - setUserId(meRes.userId || null); + setUserId(meRes.id || null); setDbError(null); onAuthSuccess({ isAdmin: !!meRes.is_admin, username: meRes.username || null, - userId: meRes.userId || null + userId: meRes.id || null }); setInternalLoggedIn(true); setTotpRequired(false); @@ -635,14 +635,29 @@ export function HomepageAuth({

{t('auth.loginWithExternalDesc')}

- + {(() => { + const isElectron = (window as any).IS_ELECTRON === true || (window as any).electronAPI?.isElectron === true; + if (isElectron) { + return ( +
+

+ {t('auth.externalNotSupportedInElectron')} +

+
+ ); + } else { + return ( + + ); + } + })()} )} {tab === "reset" && ( diff --git a/src/ui/Desktop/Navigation/LeftSidebar.tsx b/src/ui/Desktop/Navigation/LeftSidebar.tsx index 072c88d6..7f54867a 100644 --- a/src/ui/Desktop/Navigation/LeftSidebar.tsx +++ b/src/ui/Desktop/Navigation/LeftSidebar.tsx @@ -6,6 +6,7 @@ import { Hammer, ChevronUp, User2, HardDrive, Trash2, Users, Shield, Settings, Menu, ChevronRight } from "lucide-react"; import { useTranslation } from 'react-i18next'; +import {getCookie, setCookie} from "@/ui/main-axios.ts"; import { Sidebar, @@ -86,17 +87,18 @@ interface SidebarProps { } function handleLogout() { - document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; + // Clear the JWT token using the proper cookie functions + const isElectron = (window as any).IS_ELECTRON === true || (window as any).electronAPI?.isElectron === true; + + if (isElectron) { + localStorage.removeItem('jwt'); + } else { + document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; + } + window.location.reload(); } -function getCookie(name: string) { - return document.cookie.split('; ').reduce((r, v) => { - const parts = v.split('='); - return parts[0] === name ? decodeURIComponent(parts[1]) : r; - }, ""); -} - export function LeftSidebar({ diff --git a/src/ui/Mobile/Apps/Terminal/Terminal.tsx b/src/ui/Mobile/Apps/Terminal/Terminal.tsx index 5225ef25..efe1d23e 100644 --- a/src/ui/Mobile/Apps/Terminal/Terminal.tsx +++ b/src/ui/Mobile/Apps/Terminal/Terminal.tsx @@ -225,7 +225,7 @@ export const Terminal = forwardRef(function SSHTerminal( const baseUrl = (window as any).configuredServerUrl || 'http://127.0.0.1:8081'; // Convert HTTP/HTTPS to WS/WSS and use nginx reverse proxy path const wsProtocol = baseUrl.startsWith('https://') ? 'wss://' : 'ws://'; - const wsHost = baseUrl.replace(/^https?:\/\//, '').replace(/:\d+$/, ''); // Remove port if present + const wsHost = baseUrl.replace(/^https?:\/\//, ''); // Keep the port return `${wsProtocol}${wsHost}/ssh/websocket/`; })() : `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ssh/websocket/`;