Add electron server configurator
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
const { app, BrowserWindow, shell, ipcMain } = require('electron');
|
const { app, BrowserWindow, shell, ipcMain } = require('electron');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
let mainWindow = null;
|
let mainWindow = null;
|
||||||
|
|
||||||
@@ -34,7 +35,8 @@ function createWindow() {
|
|||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
webSecurity: !isDev
|
webSecurity: !isDev,
|
||||||
|
preload: path.join(__dirname, 'preload.js')
|
||||||
},
|
},
|
||||||
show: false
|
show: false
|
||||||
});
|
});
|
||||||
@@ -90,6 +92,75 @@ ipcMain.handle('get-platform', () => {
|
|||||||
return process.platform;
|
return process.platform;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Server configuration handlers
|
||||||
|
ipcMain.handle('get-server-config', () => {
|
||||||
|
try {
|
||||||
|
const userDataPath = app.getPath('userData');
|
||||||
|
const configPath = path.join(userDataPath, 'server-config.json');
|
||||||
|
|
||||||
|
if (fs.existsSync(configPath)) {
|
||||||
|
const configData = fs.readFileSync(configPath, 'utf8');
|
||||||
|
return JSON.parse(configData);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading server config:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('save-server-config', (event, config) => {
|
||||||
|
try {
|
||||||
|
const userDataPath = app.getPath('userData');
|
||||||
|
const configPath = path.join(userDataPath, 'server-config.json');
|
||||||
|
|
||||||
|
// Ensure userData directory exists
|
||||||
|
if (!fs.existsSync(userDataPath)) {
|
||||||
|
fs.mkdirSync(userDataPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving server config:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('test-server-connection', async (event, serverUrl) => {
|
||||||
|
try {
|
||||||
|
const { default: fetch } = await import('node-fetch');
|
||||||
|
|
||||||
|
// Try multiple endpoints to test the connection
|
||||||
|
const testUrls = [
|
||||||
|
`${serverUrl}/health`,
|
||||||
|
`${serverUrl}/version`,
|
||||||
|
`${serverUrl}/users/registration-allowed`
|
||||||
|
];
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
} catch (urlError) {
|
||||||
|
// Continue to next URL if this one fails
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, error: 'Server is not responding or not a valid Termix server' };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
createWindow();
|
createWindow();
|
||||||
console.log('Termix started successfully');
|
console.log('Termix started successfully');
|
||||||
|
|||||||
37
electron/preload.js
Normal file
37
electron/preload.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
|
console.log('Preload script loaded');
|
||||||
|
|
||||||
|
// Expose protected methods that allow the renderer process to use
|
||||||
|
// the ipcRenderer without exposing the entire object
|
||||||
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
// App info
|
||||||
|
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
||||||
|
getPlatform: () => ipcRenderer.invoke('get-platform'),
|
||||||
|
|
||||||
|
// Server configuration
|
||||||
|
getServerConfig: () => ipcRenderer.invoke('get-server-config'),
|
||||||
|
saveServerConfig: (config) => ipcRenderer.invoke('save-server-config', config),
|
||||||
|
testServerConnection: (serverUrl) => ipcRenderer.invoke('test-server-connection', serverUrl),
|
||||||
|
|
||||||
|
// File dialogs
|
||||||
|
showSaveDialog: (options) => ipcRenderer.invoke('show-save-dialog', options),
|
||||||
|
showOpenDialog: (options) => ipcRenderer.invoke('show-open-dialog', options),
|
||||||
|
|
||||||
|
// Update events
|
||||||
|
onUpdateAvailable: (callback) => ipcRenderer.on('update-available', callback),
|
||||||
|
onUpdateDownloaded: (callback) => ipcRenderer.on('update-downloaded', callback),
|
||||||
|
|
||||||
|
// Utility
|
||||||
|
removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel),
|
||||||
|
isElectron: true,
|
||||||
|
isDev: process.env.NODE_ENV === 'development',
|
||||||
|
|
||||||
|
// Generic invoke method
|
||||||
|
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also set the legacy IS_ELECTRON flag for backward compatibility
|
||||||
|
window.IS_ELECTRON = true;
|
||||||
|
|
||||||
|
console.log('electronAPI exposed to window');
|
||||||
@@ -151,6 +151,24 @@
|
|||||||
"failedToLoadAlerts": "Failed to load alerts",
|
"failedToLoadAlerts": "Failed to load alerts",
|
||||||
"failedToDismissAlert": "Failed to dismiss alert"
|
"failedToDismissAlert": "Failed to dismiss alert"
|
||||||
},
|
},
|
||||||
|
"serverConfig": {
|
||||||
|
"title": "Server Configuration",
|
||||||
|
"description": "Configure the Termix server URL to connect to your backend services",
|
||||||
|
"serverUrl": "Server URL",
|
||||||
|
"enterServerUrl": "Please enter a server URL",
|
||||||
|
"testConnectionFirst": "Please test the connection first",
|
||||||
|
"connectionSuccess": "Connection successful!",
|
||||||
|
"connectionFailed": "Connection failed",
|
||||||
|
"connectionError": "Connection error occurred",
|
||||||
|
"connected": "Connected",
|
||||||
|
"disconnected": "Disconnected",
|
||||||
|
"configSaved": "Configuration saved successfully",
|
||||||
|
"saveFailed": "Failed to save configuration",
|
||||||
|
"saveError": "Error saving configuration",
|
||||||
|
"saving": "Saving...",
|
||||||
|
"saveConfig": "Save Configuration",
|
||||||
|
"helpText": "Enter the URL where your Termix server is running (e.g., http://localhost:8081 or https://your-server.com)"
|
||||||
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"online": "Online",
|
"online": "Online",
|
||||||
@@ -212,6 +230,7 @@
|
|||||||
"email": "Email",
|
"email": "Email",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
"change": "Change",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
|
|||||||
@@ -152,6 +152,24 @@
|
|||||||
"failedToLoadAlerts": "加载警报失败",
|
"failedToLoadAlerts": "加载警报失败",
|
||||||
"failedToDismissAlert": "关闭警报失败"
|
"failedToDismissAlert": "关闭警报失败"
|
||||||
},
|
},
|
||||||
|
"serverConfig": {
|
||||||
|
"title": "服务器配置",
|
||||||
|
"description": "配置 Termix 服务器 URL 以连接到您的后端服务",
|
||||||
|
"serverUrl": "服务器 URL",
|
||||||
|
"enterServerUrl": "请输入服务器 URL",
|
||||||
|
"testConnectionFirst": "请先测试连接",
|
||||||
|
"connectionSuccess": "连接成功!",
|
||||||
|
"connectionFailed": "连接失败",
|
||||||
|
"connectionError": "连接发生错误",
|
||||||
|
"connected": "已连接",
|
||||||
|
"disconnected": "未连接",
|
||||||
|
"configSaved": "配置保存成功",
|
||||||
|
"saveFailed": "保存配置失败",
|
||||||
|
"saveError": "保存配置时出错",
|
||||||
|
"saving": "保存中...",
|
||||||
|
"saveConfig": "保存配置",
|
||||||
|
"helpText": "输入您的 Termix 服务器运行地址(例如:http://localhost:8081 或 https://your-server.com)"
|
||||||
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
"online": "在线",
|
"online": "在线",
|
||||||
@@ -212,6 +230,7 @@
|
|||||||
"email": "邮箱",
|
"email": "邮箱",
|
||||||
"submit": "提交",
|
"submit": "提交",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
|
"change": "更改",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"edit": "编辑",
|
"edit": "编辑",
|
||||||
|
|||||||
9
src/types/electron.d.ts
vendored
9
src/types/electron.d.ts
vendored
@@ -1,18 +1,17 @@
|
|||||||
interface ElectronAPI {
|
interface ElectronAPI {
|
||||||
getBackendPort: () => Promise<number>;
|
|
||||||
getAppVersion: () => Promise<string>;
|
getAppVersion: () => Promise<string>;
|
||||||
getPlatform: () => Promise<string>;
|
getPlatform: () => Promise<string>;
|
||||||
restartBackend: () => Promise<{ success: boolean; port?: number; error?: string }>;
|
getServerConfig: () => Promise<{ serverUrl: string; lastUpdated: string } | null>;
|
||||||
|
saveServerConfig: (config: { serverUrl: string; lastUpdated: string }) => Promise<{ success: boolean; error?: string }>;
|
||||||
|
testServerConnection: (serverUrl: string) => Promise<{ success: boolean; error?: string; status?: number }>;
|
||||||
showSaveDialog: (options: any) => Promise<any>;
|
showSaveDialog: (options: any) => Promise<any>;
|
||||||
showOpenDialog: (options: any) => Promise<any>;
|
showOpenDialog: (options: any) => Promise<any>;
|
||||||
onBackendStarted: (callback: (data: { port: number }) => void) => void;
|
|
||||||
onBackendLog: (callback: (data: string) => void) => void;
|
|
||||||
onBackendError: (callback: (data: string) => void) => void;
|
|
||||||
onUpdateAvailable: (callback: () => void) => void;
|
onUpdateAvailable: (callback: () => void) => void;
|
||||||
onUpdateDownloaded: (callback: () => void) => void;
|
onUpdateDownloaded: (callback: () => void) => void;
|
||||||
removeAllListeners: (channel: string) => void;
|
removeAllListeners: (channel: string) => void;
|
||||||
isElectron: boolean;
|
isElectron: boolean;
|
||||||
isDev: boolean;
|
isDev: boolean;
|
||||||
|
invoke: (channel: string, ...args: any[]) => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|||||||
@@ -172,7 +172,14 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
const wsUrl = isDev
|
const wsUrl = isDev
|
||||||
? 'ws://localhost:8082'
|
? 'ws://localhost:8082'
|
||||||
: isElectron
|
: isElectron
|
||||||
? 'ws://127.0.0.1:8082'
|
? (() => {
|
||||||
|
// Get configured server URL from window object (set by main-axios)
|
||||||
|
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
|
||||||
|
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/`;
|
||||||
|
|
||||||
const ws = new WebSocket(wsUrl);
|
const ws = new WebSocket(wsUrl);
|
||||||
@@ -405,7 +412,14 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
const wsUrl = isDev
|
const wsUrl = isDev
|
||||||
? 'ws://localhost:8082'
|
? 'ws://localhost:8082'
|
||||||
: isElectron
|
: isElectron
|
||||||
? 'ws://127.0.0.1:8082'
|
? (() => {
|
||||||
|
// Get configured server URL from window object (set by main-axios)
|
||||||
|
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
|
||||||
|
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/`;
|
||||||
|
|
||||||
connectToHost(cols, rows);
|
connectToHost(cols, rows);
|
||||||
|
|||||||
217
src/ui/Desktop/ElectronOnly/ServerConfig.tsx
Normal file
217
src/ui/Desktop/ElectronOnly/ServerConfig.tsx
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button.tsx';
|
||||||
|
import { Input } from '@/components/ui/input.tsx';
|
||||||
|
import { Label } from '@/components/ui/label.tsx';
|
||||||
|
import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert.tsx';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { getServerConfig, saveServerConfig, testServerConnection, type ServerConfig } from '@/ui/main-axios.ts';
|
||||||
|
import { CheckCircle, XCircle, Server, Wifi } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ServerConfigProps {
|
||||||
|
onServerConfigured: (serverUrl: string) => void;
|
||||||
|
onCancel?: () => void;
|
||||||
|
isFirstTime?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ServerConfig({ onServerConfigured, onCancel, isFirstTime = false }: ServerConfigProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [serverUrl, setServerUrl] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [testing, setTesting] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [connectionStatus, setConnectionStatus] = useState<'unknown' | 'success' | 'error'>('unknown');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadServerConfig();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadServerConfig = async () => {
|
||||||
|
try {
|
||||||
|
const config = await getServerConfig();
|
||||||
|
if (config?.serverUrl) {
|
||||||
|
setServerUrl(config.serverUrl);
|
||||||
|
setConnectionStatus('success');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load server config:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTestConnection = async () => {
|
||||||
|
if (!serverUrl.trim()) {
|
||||||
|
setError(t('serverConfig.enterServerUrl'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTesting(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Normalize URL
|
||||||
|
let normalizedUrl = serverUrl.trim();
|
||||||
|
if (!normalizedUrl.startsWith('http://') && !normalizedUrl.startsWith('https://')) {
|
||||||
|
normalizedUrl = `http://${normalizedUrl}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await testServerConnection(normalizedUrl);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setConnectionStatus('success');
|
||||||
|
} else {
|
||||||
|
setConnectionStatus('error');
|
||||||
|
setError(result.error || t('serverConfig.connectionFailed'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setConnectionStatus('error');
|
||||||
|
setError(t('serverConfig.connectionError'));
|
||||||
|
} finally {
|
||||||
|
setTesting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveConfig = async () => {
|
||||||
|
if (!serverUrl.trim()) {
|
||||||
|
setError(t('serverConfig.enterServerUrl'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectionStatus !== 'success') {
|
||||||
|
setError(t('serverConfig.testConnectionFirst'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Normalize URL
|
||||||
|
let normalizedUrl = serverUrl.trim();
|
||||||
|
if (!normalizedUrl.startsWith('http://') && !normalizedUrl.startsWith('https://')) {
|
||||||
|
normalizedUrl = `http://${normalizedUrl}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: ServerConfig = {
|
||||||
|
serverUrl: normalizedUrl,
|
||||||
|
lastUpdated: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
const success = await saveServerConfig(config);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
onServerConfigured(normalizedUrl);
|
||||||
|
} else {
|
||||||
|
setError(t('serverConfig.saveFailed'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setError(t('serverConfig.saveError'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUrlChange = (value: string) => {
|
||||||
|
setServerUrl(value);
|
||||||
|
setConnectionStatus('unknown');
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="mx-auto mb-4 w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center">
|
||||||
|
<Server className="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl font-semibold">{t('serverConfig.title')}</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mt-2">
|
||||||
|
{t('serverConfig.description')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="server-url">{t('serverConfig.serverUrl')}</Label>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Input
|
||||||
|
id="server-url"
|
||||||
|
type="text"
|
||||||
|
placeholder="http://localhost:8081 or https://your-server.com"
|
||||||
|
value={serverUrl}
|
||||||
|
onChange={(e) => handleUrlChange(e.target.value)}
|
||||||
|
className="flex-1 h-10"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleTestConnection}
|
||||||
|
disabled={testing || !serverUrl.trim() || loading}
|
||||||
|
className="w-10 h-10 p-0 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
{testing ? (
|
||||||
|
<div className="w-4 h-4 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Wifi className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{connectionStatus !== 'unknown' && (
|
||||||
|
<div className="flex items-center space-x-2 text-sm">
|
||||||
|
{connectionStatus === 'success' ? (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||||
|
<span className="text-green-600">{t('serverConfig.connected')}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<XCircle className="w-4 h-4 text-red-500" />
|
||||||
|
<span className="text-red-600">{t('serverConfig.disconnected')}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertTitle>{t('common.error')}</AlertTitle>
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
{onCancel && !isFirstTime && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={onCancel}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className={onCancel && !isFirstTime ? "flex-1" : "w-full"}
|
||||||
|
onClick={handleSaveConfig}
|
||||||
|
disabled={loading || testing || connectionStatus !== 'success'}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||||
|
<span>{t('serverConfig.saving')}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
t('serverConfig.saveConfig')
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-xs text-muted-foreground text-center">
|
||||||
|
{t('serverConfig.helpText')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -20,8 +20,11 @@ import {
|
|||||||
completePasswordReset,
|
completePasswordReset,
|
||||||
getOIDCAuthorizeUrl,
|
getOIDCAuthorizeUrl,
|
||||||
verifyTOTPLogin,
|
verifyTOTPLogin,
|
||||||
setCookie
|
setCookie,
|
||||||
|
getServerConfig,
|
||||||
|
type ServerConfig
|
||||||
} from "../../main-axios.ts";
|
} from "../../main-axios.ts";
|
||||||
|
import {ServerConfig as ServerConfigComponent} from "@/ui/Desktop/ElectronOnly/ServerConfig.tsx";
|
||||||
|
|
||||||
function getCookie(name: string) {
|
function getCookie(name: string) {
|
||||||
return document.cookie.split('; ').reduce((r, v) => {
|
return document.cookie.split('; ').reduce((r, v) => {
|
||||||
@@ -410,6 +413,66 @@ export function HomepageAuth({
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Check if we need to show server config for Electron
|
||||||
|
const [showServerConfig, setShowServerConfig] = useState<boolean | null>(null);
|
||||||
|
const [currentServerUrl, setCurrentServerUrl] = useState<string>('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkServerConfig = async () => {
|
||||||
|
if ((window as any).electronAPI) {
|
||||||
|
try {
|
||||||
|
const config = await getServerConfig();
|
||||||
|
console.log('Desktop HomepageAuth - Server config check:', config);
|
||||||
|
setCurrentServerUrl(config?.serverUrl || '');
|
||||||
|
setShowServerConfig(!config || !config.serverUrl);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Desktop HomepageAuth - No server config found, showing config screen');
|
||||||
|
setShowServerConfig(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setShowServerConfig(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkServerConfig();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (showServerConfig === null) {
|
||||||
|
// Still checking
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`w-[420px] max-w-full p-6 flex flex-col bg-dark-bg border-2 border-dark-border rounded-md ${className || ''}`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center h-32">
|
||||||
|
<div className="w-6 h-6 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showServerConfig) {
|
||||||
|
console.log('Desktop HomepageAuth - SHOWING SERVER CONFIG SCREEN');
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`w-[420px] max-w-full p-6 flex flex-col bg-dark-bg border-2 border-dark-border rounded-md ${className || ''}`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ServerConfigComponent
|
||||||
|
onServerConfigured={() => {
|
||||||
|
console.log('Server configured, reloading page');
|
||||||
|
window.location.reload();
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
console.log('Cancelled server config, going back to login');
|
||||||
|
setShowServerConfig(false);
|
||||||
|
}}
|
||||||
|
isFirstTime={!currentServerUrl}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`w-[420px] max-w-full p-6 flex flex-col bg-dark-bg border-2 border-dark-border rounded-md ${className || ''}`}
|
className={`w-[420px] max-w-full p-6 flex flex-col bg-dark-bg border-2 border-dark-border rounded-md ${className || ''}`}
|
||||||
@@ -792,13 +855,32 @@ export function HomepageAuth({
|
|||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-6 pt-4 border-t border-dark-border">
|
<div className="mt-6 pt-4 border-t border-dark-border space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-sm text-muted-foreground">{t('common.language')}</Label>
|
<Label className="text-sm text-muted-foreground">{t('common.language')}</Label>
|
||||||
</div>
|
</div>
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
</div>
|
</div>
|
||||||
|
{(window as any).electronAPI && currentServerUrl && (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm text-muted-foreground">Server</Label>
|
||||||
|
<div className="text-xs text-muted-foreground truncate max-w-[200px]">
|
||||||
|
{currentServerUrl}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowServerConfig(true)}
|
||||||
|
className="h-8 px-3"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -220,7 +220,14 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
|||||||
const wsUrl = isDev
|
const wsUrl = isDev
|
||||||
? 'ws://localhost:8082'
|
? 'ws://localhost:8082'
|
||||||
: isElectron
|
: isElectron
|
||||||
? 'ws://127.0.0.1:8082'
|
? (() => {
|
||||||
|
// Get configured server URL from window object (set by main-axios)
|
||||||
|
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
|
||||||
|
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/`;
|
||||||
|
|
||||||
const ws = new WebSocket(wsUrl);
|
const ws = new WebSocket(wsUrl);
|
||||||
|
|||||||
@@ -259,13 +259,77 @@ const isDev = process.env.NODE_ENV === 'development' &&
|
|||||||
|
|
||||||
let apiHost = import.meta.env.VITE_API_HOST || 'localhost';
|
let apiHost = import.meta.env.VITE_API_HOST || 'localhost';
|
||||||
let apiPort = 8081;
|
let apiPort = 8081;
|
||||||
|
let configuredServerUrl: string | null = null;
|
||||||
|
|
||||||
if (isElectron) {
|
if (isElectron) {
|
||||||
apiPort = 8081;
|
apiPort = 8081;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Server configuration management for Electron
|
||||||
|
export interface ServerConfig {
|
||||||
|
serverUrl: string;
|
||||||
|
lastUpdated: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getServerConfig(): Promise<ServerConfig | null> {
|
||||||
|
if (!isElectron) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await (window as any).electronAPI?.invoke('get-server-config');
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get server config:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveServerConfig(config: ServerConfig): Promise<boolean> {
|
||||||
|
if (!isElectron) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await (window as any).electronAPI?.invoke('save-server-config', config);
|
||||||
|
if (result?.success) {
|
||||||
|
configuredServerUrl = config.serverUrl;
|
||||||
|
updateApiInstances();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save server config:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function testServerConnection(serverUrl: string): Promise<{ success: boolean; error?: string }> {
|
||||||
|
if (!isElectron) return { success: false, error: 'Not in Electron environment' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await (window as any).electronAPI?.invoke('test-server-connection', serverUrl);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to test server connection:', error);
|
||||||
|
return { success: false, error: 'Connection test failed' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize server configuration on load
|
||||||
|
if (isElectron) {
|
||||||
|
getServerConfig().then(config => {
|
||||||
|
if (config?.serverUrl) {
|
||||||
|
configuredServerUrl = config.serverUrl;
|
||||||
|
updateApiInstances();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getApiUrl(path: string, defaultPort: number): string {
|
function getApiUrl(path: string, defaultPort: number): string {
|
||||||
if (isElectron) {
|
if (isElectron) {
|
||||||
|
if (configuredServerUrl) {
|
||||||
|
// In Electron with configured server, all requests go through nginx reverse proxy
|
||||||
|
// Use the same base URL for all services (nginx routes to correct backend port)
|
||||||
|
const baseUrl = configuredServerUrl.replace(/\/$/, '');
|
||||||
|
return `${baseUrl}${path}`;
|
||||||
|
}
|
||||||
return `http://127.0.0.1:${defaultPort}${path}`;
|
return `http://127.0.0.1:${defaultPort}${path}`;
|
||||||
} else if (isDev) {
|
} else if (isDev) {
|
||||||
return `http://${apiHost}:${defaultPort}${path}`;
|
return `http://${apiHost}:${defaultPort}${path}`;
|
||||||
@@ -305,7 +369,29 @@ export let authApi = createApiInstance(
|
|||||||
'AUTH'
|
'AUTH'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Function to update API instances with new port (for Electron)
|
// Function to update API instances with new server configuration
|
||||||
|
function updateApiInstances() {
|
||||||
|
systemLogger.info('Updating API instances with new server configuration', {
|
||||||
|
operation: 'api_instance_update',
|
||||||
|
configuredServerUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
sshHostApi = createApiInstance(getApiUrl('/ssh', 8081), 'SSH_HOST');
|
||||||
|
tunnelApi = createApiInstance(getApiUrl('/ssh', 8083), 'TUNNEL');
|
||||||
|
fileManagerApi = createApiInstance(getApiUrl('/ssh/file_manager', 8084), 'FILE_MANAGER');
|
||||||
|
statsApi = createApiInstance(getApiUrl('', 8085), 'STATS');
|
||||||
|
authApi = createApiInstance(getApiUrl('', 8081), 'AUTH');
|
||||||
|
|
||||||
|
// Make configuredServerUrl available globally for components that need it
|
||||||
|
(window as any).configuredServerUrl = configuredServerUrl;
|
||||||
|
|
||||||
|
systemLogger.success('All API instances updated successfully', {
|
||||||
|
operation: 'api_instance_update_complete',
|
||||||
|
configuredServerUrl
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to update API instances with new port (for Electron) - kept for backward compatibility
|
||||||
function updateApiPorts(port: number) {
|
function updateApiPorts(port: number) {
|
||||||
systemLogger.info('Updating API instances with new port', {
|
systemLogger.info('Updating API instances with new port', {
|
||||||
operation: 'api_port_update',
|
operation: 'api_port_update',
|
||||||
@@ -313,16 +399,7 @@ function updateApiPorts(port: number) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
apiPort = port;
|
apiPort = port;
|
||||||
sshHostApi = createApiInstance(`http://127.0.0.1:${port}/ssh`, 'SSH_HOST');
|
updateApiInstances();
|
||||||
tunnelApi = createApiInstance(`http://127.0.0.1:${port}/ssh`, 'TUNNEL');
|
|
||||||
fileManagerApi = createApiInstance(`http://127.0.0.1:${port}/ssh/file_manager`, 'FILE_MANAGER');
|
|
||||||
statsApi = createApiInstance(`http://127.0.0.1:${port}`, 'STATS');
|
|
||||||
authApi = createApiInstance(`http://127.0.0.1:${port}`, 'AUTH');
|
|
||||||
|
|
||||||
systemLogger.success('All API instances updated successfully', {
|
|
||||||
operation: 'api_port_update_complete',
|
|
||||||
port
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -1104,8 +1181,7 @@ export async function generateBackupCodes(password?: string, totp_code?: string)
|
|||||||
|
|
||||||
export async function getUserAlerts(userId: string): Promise<{ alerts: any[] }> {
|
export async function getUserAlerts(userId: string): Promise<{ alerts: any[] }> {
|
||||||
try {
|
try {
|
||||||
const apiInstance = createApiInstance(isDev ? `http://${apiHost}:8081` : '');
|
const response = await authApi.get(`/alerts/user/${userId}`);
|
||||||
const response = await apiInstance.get(`/alerts/user/${userId}`);
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleApiError(error, 'fetch user alerts');
|
handleApiError(error, 'fetch user alerts');
|
||||||
@@ -1114,9 +1190,7 @@ export async function getUserAlerts(userId: string): Promise<{ alerts: any[] }>
|
|||||||
|
|
||||||
export async function dismissAlert(userId: string, alertId: string): Promise<any> {
|
export async function dismissAlert(userId: string, alertId: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
// Use the general API instance since alerts endpoint is at root level
|
const response = await authApi.post('/alerts/dismiss', { userId, alertId });
|
||||||
const apiInstance = createApiInstance(isDev ? `http://${apiHost}:8081` : '');
|
|
||||||
const response = await apiInstance.post('/alerts/dismiss', { userId, alertId });
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleApiError(error, 'dismiss alert');
|
handleApiError(error, 'dismiss alert');
|
||||||
|
|||||||
Reference in New Issue
Block a user