v1.6.0 #221
@@ -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
|
// Test the health endpoint specifically - this is required for a valid Termix server
|
||||||
const healthUrl = `${serverUrl}/health`;
|
const healthUrl = `${normalizedServerUrl}/health`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(healthUrl, {
|
const response = await fetch(healthUrl, {
|
||||||
@@ -200,11 +203,19 @@ ipcMain.handle('test-server-connection', async (event, serverUrl) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
// Try to parse the response to ensure it's a valid health check
|
|
||||||
const data = await response.text();
|
const data = await response.text();
|
||||||
// A valid health check should return some JSON or text indicating the server is healthy
|
// A valid Termix health check should return JSON with specific structure
|
||||||
if (data && (data.includes('healthy') || data.includes('ok') || data.includes('status') || response.status === 200)) {
|
try {
|
||||||
return { success: true, status: response.status, testedUrl: healthUrl };
|
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) {
|
} catch (urlError) {
|
||||||
@@ -213,7 +224,7 @@ ipcMain.handle('test-server-connection', async (event, serverUrl) => {
|
|||||||
|
|
||||||
// If health check fails, try version endpoint as fallback
|
// If health check fails, try version endpoint as fallback
|
||||||
try {
|
try {
|
||||||
const versionUrl = `${serverUrl}/version`;
|
const versionUrl = `${normalizedServerUrl}/version`;
|
||||||
const response = await fetch(versionUrl, {
|
const response = await fetch(versionUrl, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
timeout: 5000
|
timeout: 5000
|
||||||
@@ -221,9 +232,17 @@ ipcMain.handle('test-server-connection', async (event, serverUrl) => {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.text();
|
const data = await response.text();
|
||||||
// Check if it looks like a Termix version response
|
try {
|
||||||
if (data && (data.includes('version') || data.includes('termix') || data.includes('1.') || response.status === 200)) {
|
const versionData = JSON.parse(data);
|
||||||
return { success: true, status: response.status, testedUrl: versionUrl, warning: 'Health endpoint not available, but server appears to be running' };
|
// 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) {
|
} catch (versionError) {
|
||||||
|
|||||||
@@ -787,6 +787,7 @@
|
|||||||
"external": "External",
|
"external": "External",
|
||||||
"loginWithExternal": "Login with External Provider",
|
"loginWithExternal": "Login with External Provider",
|
||||||
"loginWithExternalDesc": "Login using your configured external identity 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",
|
"resetPasswordButton": "Reset Password",
|
||||||
"sendResetCode": "Send Reset Code",
|
"sendResetCode": "Send Reset Code",
|
||||||
"resetCodeDesc": "Enter your username to receive a password reset code. The code will be logged in the docker container logs.",
|
"resetCodeDesc": "Enter your username to receive a password reset code. The code will be logged in the docker container logs.",
|
||||||
|
|||||||
@@ -770,6 +770,7 @@
|
|||||||
"external": "外部",
|
"external": "外部",
|
||||||
"loginWithExternal": "使用外部提供商登录",
|
"loginWithExternal": "使用外部提供商登录",
|
||||||
"loginWithExternalDesc": "使用您配置的外部身份提供者登录",
|
"loginWithExternalDesc": "使用您配置的外部身份提供者登录",
|
||||||
|
"externalNotSupportedInElectron": "Electron 应用暂不支持外部身份验证。请使用网页版本进行 OIDC 登录。",
|
||||||
"resetPasswordButton": "重置密码",
|
"resetPasswordButton": "重置密码",
|
||||||
"sendResetCode": "发送重置代码",
|
"sendResetCode": "发送重置代码",
|
||||||
"resetCodeDesc": "输入您的用户名以接收密码重置代码。代码将记录在 docker 容器日志中。",
|
"resetCodeDesc": "输入您的用户名以接收密码重置代码。代码将记录在 docker 容器日志中。",
|
||||||
|
|||||||
@@ -79,18 +79,43 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const jwt = getCookie("jwt");
|
const jwt = getCookie("jwt");
|
||||||
if (!jwt) return;
|
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()
|
getOIDCConfig()
|
||||||
.then(res => {
|
.then(res => {
|
||||||
if (res) setOidcConfig(res);
|
if (res) setOidcConfig(res);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error('Failed to fetch OIDC config:', 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();
|
fetchUsers();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
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()
|
getRegistrationAllowed()
|
||||||
.then(res => {
|
.then(res => {
|
||||||
if (typeof res?.allowed === 'boolean') {
|
if (typeof res?.allowed === 'boolean') {
|
||||||
@@ -99,17 +124,37 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error('Failed to fetch registration status:', 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 fetchUsers = async () => {
|
||||||
const jwt = getCookie("jwt");
|
const jwt = getCookie("jwt");
|
||||||
if (!jwt) return;
|
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);
|
setUsersLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await getUserList();
|
const response = await getUserList();
|
||||||
setUsers(response.users);
|
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 {
|
} finally {
|
||||||
setUsersLoading(false);
|
setUsersLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
const baseUrl = (window as any).configuredServerUrl || 'http://127.0.0.1:8081';
|
const baseUrl = (window as any).configuredServerUrl || 'http://127.0.0.1:8081';
|
||||||
// Convert HTTP/HTTPS to WS/WSS and use nginx reverse proxy path
|
// Convert HTTP/HTTPS to WS/WSS and use nginx reverse proxy path
|
||||||
const wsProtocol = baseUrl.startsWith('https://') ? 'wss://' : 'ws://';
|
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/`;
|
return `${wsProtocol}${wsHost}/ssh/websocket/`;
|
||||||
})()
|
})()
|
||||||
: `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ssh/websocket/`;
|
: `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ssh/websocket/`;
|
||||||
|
|||||||
@@ -185,12 +185,12 @@ export function HomepageAuth({
|
|||||||
setLoggedIn(true);
|
setLoggedIn(true);
|
||||||
setIsAdmin(!!meRes.is_admin);
|
setIsAdmin(!!meRes.is_admin);
|
||||||
setUsername(meRes.username || null);
|
setUsername(meRes.username || null);
|
||||||
setUserId(meRes.userId || null);
|
setUserId(meRes.id || null);
|
||||||
setDbError(null);
|
setDbError(null);
|
||||||
onAuthSuccess({
|
onAuthSuccess({
|
||||||
isAdmin: !!meRes.is_admin,
|
isAdmin: !!meRes.is_admin,
|
||||||
username: meRes.username || null,
|
username: meRes.username || null,
|
||||||
userId: meRes.userId || null
|
userId: meRes.id || null
|
||||||
});
|
});
|
||||||
setInternalLoggedIn(true);
|
setInternalLoggedIn(true);
|
||||||
if (tab === "signup") {
|
if (tab === "signup") {
|
||||||
@@ -320,12 +320,12 @@ export function HomepageAuth({
|
|||||||
setLoggedIn(true);
|
setLoggedIn(true);
|
||||||
setIsAdmin(!!meRes.is_admin);
|
setIsAdmin(!!meRes.is_admin);
|
||||||
setUsername(meRes.username || null);
|
setUsername(meRes.username || null);
|
||||||
setUserId(meRes.userId || null);
|
setUserId(meRes.id || null);
|
||||||
setDbError(null);
|
setDbError(null);
|
||||||
onAuthSuccess({
|
onAuthSuccess({
|
||||||
isAdmin: !!meRes.is_admin,
|
isAdmin: !!meRes.is_admin,
|
||||||
username: meRes.username || null,
|
username: meRes.username || null,
|
||||||
userId: meRes.userId || null
|
userId: meRes.id || null
|
||||||
});
|
});
|
||||||
setInternalLoggedIn(true);
|
setInternalLoggedIn(true);
|
||||||
setTotpRequired(false);
|
setTotpRequired(false);
|
||||||
@@ -635,14 +635,29 @@ export function HomepageAuth({
|
|||||||
<div className="text-center text-muted-foreground mb-4">
|
<div className="text-center text-muted-foreground mb-4">
|
||||||
<p>{t('auth.loginWithExternalDesc')}</p>
|
<p>{t('auth.loginWithExternalDesc')}</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
{(() => {
|
||||||
type="button"
|
const isElectron = (window as any).IS_ELECTRON === true || (window as any).electronAPI?.isElectron === true;
|
||||||
className="w-full h-11 mt-2 text-base font-semibold"
|
if (isElectron) {
|
||||||
disabled={oidcLoading}
|
return (
|
||||||
onClick={handleOIDCLogin}
|
<div className="text-center p-4 bg-muted/50 rounded-lg border">
|
||||||
>
|
<p className="text-muted-foreground text-sm">
|
||||||
{oidcLoading ? Spinner : t('auth.loginWithExternal')}
|
{t('auth.externalNotSupportedInElectron')}
|
||||||
</Button>
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="w-full h-11 mt-2 text-base font-semibold"
|
||||||
|
disabled={oidcLoading}
|
||||||
|
onClick={handleOIDCLogin}
|
||||||
|
>
|
||||||
|
{oidcLoading ? Spinner : t('auth.loginWithExternal')}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})()}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{tab === "reset" && (
|
{tab === "reset" && (
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Hammer, ChevronUp, User2, HardDrive, Trash2, Users, Shield, Settings, Menu, ChevronRight
|
Hammer, ChevronUp, User2, HardDrive, Trash2, Users, Shield, Settings, Menu, ChevronRight
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {getCookie, setCookie} from "@/ui/main-axios.ts";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
@@ -86,17 +87,18 @@ interface SidebarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleLogout() {
|
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();
|
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({
|
export function LeftSidebar({
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
const baseUrl = (window as any).configuredServerUrl || 'http://127.0.0.1:8081';
|
const baseUrl = (window as any).configuredServerUrl || 'http://127.0.0.1:8081';
|
||||||
// Convert HTTP/HTTPS to WS/WSS and use nginx reverse proxy path
|
// Convert HTTP/HTTPS to WS/WSS and use nginx reverse proxy path
|
||||||
const wsProtocol = baseUrl.startsWith('https://') ? 'wss://' : 'ws://';
|
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/`;
|
return `${wsProtocol}${wsHost}/ssh/websocket/`;
|
||||||
})()
|
})()
|
||||||
: `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ssh/websocket/`;
|
: `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ssh/websocket/`;
|
||||||
|
|||||||
Reference in New Issue
Block a user