v1.6.0 #221

Merged
LukeGus merged 74 commits from dev-1.6.0 into main 2025-09-12 19:42:00 +00:00
8 changed files with 116 additions and 33 deletions
Showing only changes of commit 54fb8ffc24 - Show all commits

View File

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

View File

@@ -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.",

View File

@@ -770,6 +770,7 @@
"external": "外部", "external": "外部",
"loginWithExternal": "使用外部提供商登录", "loginWithExternal": "使用外部提供商登录",
"loginWithExternalDesc": "使用您配置的外部身份提供者登录", "loginWithExternalDesc": "使用您配置的外部身份提供者登录",
"externalNotSupportedInElectron": "Electron 应用暂不支持外部身份验证。请使用网页版本进行 OIDC 登录。",
"resetPasswordButton": "重置密码", "resetPasswordButton": "重置密码",
"sendResetCode": "发送重置代码", "sendResetCode": "发送重置代码",
"resetCodeDesc": "输入您的用户名以接收密码重置代码。代码将记录在 docker 容器日志中。", "resetCodeDesc": "输入您的用户名以接收密码重置代码。代码将记录在 docker 容器日志中。",

View File

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

View File

@@ -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/`;

View File

@@ -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" && (

View File

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

View File

@@ -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/`;