Add more translations, fix user delete failing
This commit is contained in:
@@ -14,7 +14,9 @@
|
||||
},
|
||||
"homepage": {
|
||||
"loggedInTitle": "Logged in!",
|
||||
"loggedInMessage": "You are logged in! Use the sidebar to access all available tools. To get started, create an SSH Host in the SSH Manager tab. Once created, you can connect to that host using the other apps in the sidebar."
|
||||
"loggedInMessage": "You are logged in! Use the sidebar to access all available tools. To get started, create an SSH Host in the SSH Manager tab. Once created, you can connect to that host using the other apps in the sidebar.",
|
||||
"failedToLoadAlerts": "Failed to load alerts",
|
||||
"failedToDismissAlert": "Failed to dismiss alert"
|
||||
},
|
||||
"common": {
|
||||
"close": "Close",
|
||||
@@ -34,7 +36,14 @@
|
||||
"sidebar": "Sidebar",
|
||||
"home": "Home",
|
||||
"expired": "Expired",
|
||||
"expiresToday": "Expires today",
|
||||
"expiresTomorrow": "Expires tomorrow",
|
||||
"expiresInDays": "Expires in {{days}} days",
|
||||
"updateAvailable": "Update Available",
|
||||
"sshPath": "SSH Path",
|
||||
"localPath": "Local Path",
|
||||
"loading": "Loading...",
|
||||
"noAuthCredentials": "No authentication credentials available for this SSH host",
|
||||
"noReleases": "No Releases",
|
||||
"updatesAndReleases": "Updates & Releases",
|
||||
"newVersionAvailable": "A new version ({{version}}) is available.",
|
||||
@@ -402,7 +411,15 @@
|
||||
"enterFolderPath": "Enter folder path",
|
||||
"noShortcuts": "No shortcuts.",
|
||||
"searchFilesAndFolders": "Search files and folders...",
|
||||
"noFilesOrFoldersFound": "No files or folders found."
|
||||
"noFilesOrFoldersFound": "No files or folders found.",
|
||||
"failedToConnectSSH": "Failed to connect to SSH",
|
||||
"failedToReconnectSSH": "Failed to reconnect SSH session",
|
||||
"failedToListFiles": "Failed to list files",
|
||||
"fetchHomeDataTimeout": "Fetch home data timed out",
|
||||
"sshStatusCheckTimeout": "SSH status check timed out",
|
||||
"sshReconnectionTimeout": "SSH reconnection timed out",
|
||||
"saveOperationTimeout": "Save operation timed out",
|
||||
"cannotSaveFile": "Cannot save file"
|
||||
},
|
||||
"tunnels": {
|
||||
"title": "SSH Tunnels",
|
||||
@@ -457,7 +474,8 @@
|
||||
"portMapping": "Port {{sourcePort}} → {{endpointHost}}:{{endpointPort}}",
|
||||
"disconnect": "Disconnect",
|
||||
"connect": "Connect",
|
||||
"canceling": "Canceling..."
|
||||
"canceling": "Canceling...",
|
||||
"endpointHostNotFound": "Endpoint host not found"
|
||||
},
|
||||
"serverStats": {
|
||||
"title": "Server Statistics",
|
||||
@@ -509,7 +527,7 @@
|
||||
"passwordReset": "Password reset link sent",
|
||||
"twoFactorAuth": "Two-Factor Authentication",
|
||||
"enterCode": "Enter verification code",
|
||||
"backupCode": "Use backup code",
|
||||
"backupCode": "Or use backup code",
|
||||
"verifyCode": "Verify Code",
|
||||
"enableTwoFactor": "Enable Two-Factor Authentication",
|
||||
"disableTwoFactor": "Disable Two-Factor Authentication",
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
},
|
||||
"homepage": {
|
||||
"loggedInTitle": "登录成功!",
|
||||
"loggedInMessage": "您已登录!使用侧边栏访问所有可用工具。要开始使用,请在 SSH 管理器选项卡中创建 SSH 主机。创建后,您可以使用侧边栏中的其他应用程序连接到该主机。"
|
||||
"loggedInMessage": "您已登录!使用侧边栏访问所有可用工具。要开始使用,请在 SSH 管理器选项卡中创建 SSH 主机。创建后,您可以使用侧边栏中的其他应用程序连接到该主机。",
|
||||
"failedToLoadAlerts": "加载警报失败",
|
||||
"failedToDismissAlert": "关闭警报失败"
|
||||
},
|
||||
"common": {
|
||||
"close": "关闭",
|
||||
@@ -34,7 +36,14 @@
|
||||
"sidebar": "侧边栏",
|
||||
"home": "首页",
|
||||
"expired": "已过期",
|
||||
"expiresToday": "今天过期",
|
||||
"expiresTomorrow": "明天过期",
|
||||
"expiresInDays": "{{days}} 天后过期",
|
||||
"updateAvailable": "有可用更新",
|
||||
"sshPath": "SSH 路径",
|
||||
"localPath": "本地路径",
|
||||
"loading": "加载中...",
|
||||
"noAuthCredentials": "此 SSH 主机没有可用的身份验证凭据",
|
||||
"noReleases": "没有发布版本",
|
||||
"updatesAndReleases": "更新与发布",
|
||||
"newVersionAvailable": "有新版本 ({{version}}) 可用。",
|
||||
@@ -439,7 +448,15 @@
|
||||
"enterFolderPath": "输入文件夹路径",
|
||||
"noShortcuts": "没有快捷方式。",
|
||||
"searchFilesAndFolders": "搜索文件和文件夹...",
|
||||
"noFilesOrFoldersFound": "没有找到文件或文件夹。"
|
||||
"noFilesOrFoldersFound": "没有找到文件或文件夹。",
|
||||
"failedToConnectSSH": "连接 SSH 失败",
|
||||
"failedToReconnectSSH": "重新连接 SSH 会话失败",
|
||||
"failedToListFiles": "列出文件失败",
|
||||
"fetchHomeDataTimeout": "获取主页数据超时",
|
||||
"sshStatusCheckTimeout": "SSH 状态检查超时",
|
||||
"sshReconnectionTimeout": "SSH 重新连接超时",
|
||||
"saveOperationTimeout": "保存操作超时",
|
||||
"cannotSaveFile": "无法保存文件"
|
||||
},
|
||||
"tunnels": {
|
||||
"title": "SSH 隧道",
|
||||
@@ -494,7 +511,8 @@
|
||||
"portMapping": "端口 {{sourcePort}} → {{endpointHost}}:{{endpointPort}}",
|
||||
"disconnect": "断开连接",
|
||||
"connect": "连接",
|
||||
"canceling": "取消中..."
|
||||
"canceling": "取消中...",
|
||||
"endpointHostNotFound": "未找到端点主机"
|
||||
},
|
||||
"serverStats": {
|
||||
"title": "服务器统计",
|
||||
|
||||
@@ -1311,17 +1311,26 @@ router.delete('/delete-user', authenticateJWT, async (req, res) => {
|
||||
const targetUserId = targetUser[0].id;
|
||||
|
||||
try {
|
||||
// Delete from tables that have foreign keys to both users and ssh_data first
|
||||
// These must be deleted before ssh_data because they reference host_id
|
||||
await db.delete(fileManagerRecent).where(eq(fileManagerRecent.userId, targetUserId));
|
||||
await db.delete(fileManagerPinned).where(eq(fileManagerPinned.userId, targetUserId));
|
||||
await db.delete(fileManagerShortcuts).where(eq(fileManagerShortcuts.userId, targetUserId));
|
||||
|
||||
// Delete from tables that only reference users
|
||||
await db.delete(dismissedAlerts).where(eq(dismissedAlerts.userId, targetUserId));
|
||||
|
||||
// Delete from ssh_data (which references users)
|
||||
await db.delete(sshData).where(eq(sshData.userId, targetUserId));
|
||||
|
||||
// Delete from any additional tables that might exist
|
||||
db.$client.prepare('DELETE FROM config_editor_recent WHERE user_id = ?').run(targetUserId);
|
||||
db.$client.prepare('DELETE FROM config_editor_pinned WHERE user_id = ?').run(targetUserId);
|
||||
db.$client.prepare('DELETE FROM config_editor_shortcuts WHERE user_id = ?').run(targetUserId);
|
||||
db.$client.prepare('DELETE FROM shared_hosts WHERE original_user_id = ? OR shared_with_user_id = ?').run(targetUserId, targetUserId);
|
||||
await db.delete(fileManagerRecent).where(eq(fileManagerRecent.userId, targetUserId));
|
||||
await db.delete(fileManagerPinned).where(eq(fileManagerPinned.userId, targetUserId));
|
||||
await db.delete(fileManagerShortcuts).where(eq(fileManagerShortcuts.userId, targetUserId));
|
||||
await db.delete(dismissedAlerts).where(eq(dismissedAlerts.userId, targetUserId));
|
||||
await db.delete(sshData).where(eq(sshData.userId, targetUserId));
|
||||
} catch (cleanupError) {
|
||||
logger.error(`Cleanup failed for user ${username}:`, cleanupError);
|
||||
throw cleanupError; // Re-throw to prevent user deletion if cleanup fails
|
||||
}
|
||||
|
||||
await db.delete(users).where(eq(users.id, targetUserId));
|
||||
|
||||
@@ -136,7 +136,7 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
]);
|
||||
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Fetch home data timed out')), 15000)
|
||||
setTimeout(() => reject(new Error(t('fileManager.fetchHomeDataTimeout'))), 15000)
|
||||
);
|
||||
|
||||
const [recentRes, pinnedRes, shortcutsRes] = await Promise.race([homeDataPromise, timeoutPromise]) as [any, any, any];
|
||||
@@ -371,7 +371,7 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
try {
|
||||
const statusPromise = getSSHStatus(tab.sshSessionId);
|
||||
const statusTimeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('SSH status check timed out')), 10000)
|
||||
setTimeout(() => reject(new Error(t('fileManager.sshStatusCheckTimeout'))), 10000)
|
||||
);
|
||||
|
||||
const status = await Promise.race([statusPromise, statusTimeoutPromise]) as { connected: boolean };
|
||||
@@ -386,7 +386,7 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
keyPassword: currentHost.keyPassword
|
||||
});
|
||||
const connectTimeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('SSH reconnection timed out')), 15000)
|
||||
setTimeout(() => reject(new Error(t('fileManager.sshReconnectionTimeout'))), 15000)
|
||||
);
|
||||
|
||||
await Promise.race([connectPromise, connectTimeoutPromise]);
|
||||
@@ -397,7 +397,7 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
const savePromise = writeSSHFile(tab.sshSessionId, tab.filePath, tab.content);
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => {
|
||||
reject(new Error('Save operation timed out'));
|
||||
reject(new Error(t('fileManager.saveOperationTimeout')));
|
||||
}, 30000)
|
||||
);
|
||||
|
||||
@@ -432,7 +432,7 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
let errorMessage = formatErrorMessage(err, 'Cannot save file');
|
||||
let errorMessage = formatErrorMessage(err, t('fileManager.cannotSaveFile'));
|
||||
|
||||
if (errorMessage.includes('timed out') || errorMessage.includes('timeout')) {
|
||||
errorMessage = t('fileManager.saveTimeout');
|
||||
|
||||
@@ -128,7 +128,7 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
|
||||
|
||||
try {
|
||||
if (!server.password && !server.key) {
|
||||
toast.error('No authentication credentials available for this SSH host');
|
||||
toast.error(t('common.noAuthCredentials'));
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
|
||||
|
||||
return sessionId;
|
||||
} catch (err: any) {
|
||||
toast.error(err?.response?.data?.error || 'Failed to connect to SSH');
|
||||
toast.error(err?.response?.data?.error || t('fileManager.failedToConnectSSH'));
|
||||
setSshSessionId(null);
|
||||
return null;
|
||||
} finally {
|
||||
@@ -189,7 +189,7 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
|
||||
setSshSessionId(newSessionId);
|
||||
res = await listSSHFiles(newSessionId, currentPath);
|
||||
} else {
|
||||
throw new Error('Failed to reconnect SSH session');
|
||||
throw new Error(t('fileManager.failedToReconnectSSH'));
|
||||
}
|
||||
} else {
|
||||
res = await listSSHFiles(sshSessionId, currentPath);
|
||||
@@ -220,7 +220,7 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
|
||||
}
|
||||
} catch (err: any) {
|
||||
setFiles([]);
|
||||
toast.error(err?.response?.data?.error || err?.message || 'Failed to list files');
|
||||
toast.error(err?.response?.data?.error || err?.message || t('fileManager.failedToListFiles'));
|
||||
} finally {
|
||||
setFilesLoading(false);
|
||||
setFetchingFiles(false);
|
||||
@@ -334,7 +334,7 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
|
||||
fetchFiles();
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(error?.response?.data?.error || 'Failed to rename item');
|
||||
toast.error(error?.response?.data?.error || t('fileManager.failedToRenameItem'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -350,7 +350,7 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
|
||||
fetchFiles();
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(error?.response?.data?.error || 'Failed to delete item');
|
||||
toast.error(error?.response?.data?.error || t('fileManager.failedToDeleteItem'));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {Button} from '@/components/ui/button.tsx';
|
||||
import {Card} from '@/components/ui/card.tsx';
|
||||
import {Separator} from '@/components/ui/separator.tsx';
|
||||
import {Plus, Folder, File, Star, Trash2, Edit, Link2, Server, Pin} from 'lucide-react';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
|
||||
interface SSHConnection {
|
||||
id: string;
|
||||
@@ -61,16 +62,18 @@ export function FileManagerLeftSidebarFileViewer({
|
||||
onSwitchToSSH,
|
||||
currentSSH,
|
||||
}: FileManagerLeftSidebarVileViewerProps) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-1 bg-[#09090b] p-2 overflow-y-auto">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span
|
||||
className="text-xs text-muted-foreground font-semibold">{isSSHMode ? 'SSH Path' : 'Local Path'}</span>
|
||||
className="text-xs text-muted-foreground font-semibold">{isSSHMode ? t('common.sshPath') : t('common.localPath')}</span>
|
||||
<span className="text-xs text-white truncate">{currentPath}</span>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="text-xs text-muted-foreground">Loading...</div>
|
||||
<div className="text-xs text-muted-foreground">{t('common.loading')}</div>
|
||||
) : error ? (
|
||||
<div className="text-xs text-red-500">{error}</div>
|
||||
) : (
|
||||
|
||||
@@ -3,6 +3,7 @@ import {Card, CardContent, CardFooter, CardHeader, CardTitle} from "@/components
|
||||
import {Button} from "@/components/ui/button.tsx";
|
||||
import {Badge} from "@/components/ui/badge.tsx";
|
||||
import {X, ExternalLink, AlertTriangle, Info, CheckCircle, AlertCircle} from "lucide-react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
interface TermixAlert {
|
||||
id: string;
|
||||
@@ -64,6 +65,8 @@ const getTypeBadgeVariant = (type?: string) => {
|
||||
};
|
||||
|
||||
export function HomepageAlertCard({alert, onDismiss, onClose}: AlertCardProps): React.ReactElement {
|
||||
const {t} = useTranslation();
|
||||
|
||||
if (!alert) {
|
||||
return null;
|
||||
}
|
||||
@@ -79,10 +82,10 @@ export function HomepageAlertCard({alert, onDismiss, onClose}: AlertCardProps):
|
||||
const diffTime = expiryDate.getTime() - now.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays < 0) return 'Expired';
|
||||
if (diffDays === 0) return 'Expires today';
|
||||
if (diffDays === 1) return 'Expires tomorrow';
|
||||
return `Expires in ${diffDays} days`;
|
||||
if (diffDays < 0) return t('common.expired');
|
||||
if (diffDays === 0) return t('common.expiresToday');
|
||||
if (diffDays === 1) return t('common.expiresTomorrow');
|
||||
return t('common.expiresInDays', {days: diffDays});
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, {useEffect, useState} from "react";
|
||||
import {HomepageAlertCard} from "./HomepageAlertCard.tsx";
|
||||
import {Button} from "@/components/ui/button.tsx";
|
||||
import { getUserAlerts, dismissAlert } from "@/ui/main-axios.ts";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
interface TermixAlert {
|
||||
id: string;
|
||||
@@ -20,6 +21,7 @@ interface AlertManagerProps {
|
||||
}
|
||||
|
||||
export function HomepageAlertManager({userId, loggedIn}: AlertManagerProps): React.ReactElement {
|
||||
const {t} = useTranslation();
|
||||
const [alerts, setAlerts] = useState<TermixAlert[]>([]);
|
||||
const [currentAlertIndex, setCurrentAlertIndex] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -57,7 +59,7 @@ export function HomepageAlertManager({userId, loggedIn}: AlertManagerProps): Rea
|
||||
setAlerts(sortedAlerts);
|
||||
setCurrentAlertIndex(0);
|
||||
} catch (err) {
|
||||
setError('Failed to load alerts');
|
||||
setError(t('homepage.failedToLoadAlerts'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -81,7 +83,7 @@ export function HomepageAlertManager({userId, loggedIn}: AlertManagerProps): Rea
|
||||
return prevIndex;
|
||||
});
|
||||
} catch (err) {
|
||||
setError('Failed to dismiss alert');
|
||||
setError(t('homepage.failedToDismissAlert'));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ export function LeftSidebar({
|
||||
const sshManagerTab = tabList.find((t) => t.type === 'ssh_manager');
|
||||
const openSshManagerTab = () => {
|
||||
if (sshManagerTab || isSplitScreenActive) return;
|
||||
const id = addTab({type: 'ssh_manager', title: 'SSH Manager'} as any);
|
||||
const id = addTab({type: 'ssh_manager'} as any);
|
||||
setCurrentTab(id);
|
||||
};
|
||||
const adminTab = tabList.find((t) => t.type === 'admin');
|
||||
@@ -130,7 +130,7 @@ export function LeftSidebar({
|
||||
setCurrentTab(adminTab.id);
|
||||
return;
|
||||
}
|
||||
const id = addTab({type: 'admin', title: 'Admin'} as any);
|
||||
const id = addTab({type: 'admin'} as any);
|
||||
setCurrentTab(id);
|
||||
};
|
||||
|
||||
@@ -186,7 +186,7 @@ export function LeftSidebar({
|
||||
}, 50);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setHostsError('Failed to load hosts');
|
||||
setHostsError(t('leftSidebar.failedToLoadHosts'));
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -229,7 +229,7 @@ export function LeftSidebar({
|
||||
const hostsByFolder = React.useMemo(() => {
|
||||
const map: Record<string, SSHHost[]> = {};
|
||||
filteredHosts.forEach(h => {
|
||||
const folder = h.folder && h.folder.trim() ? h.folder : 'No Folder';
|
||||
const folder = h.folder && h.folder.trim() ? h.folder : t('leftSidebar.noFolder');
|
||||
if (!map[folder]) map[folder] = [];
|
||||
map[folder].push(h);
|
||||
});
|
||||
@@ -239,8 +239,8 @@ export function LeftSidebar({
|
||||
const sortedFolders = React.useMemo(() => {
|
||||
const folders = Object.keys(hostsByFolder);
|
||||
folders.sort((a, b) => {
|
||||
if (a === 'No Folder') return -1;
|
||||
if (b === 'No Folder') return 1;
|
||||
if (a === t('leftSidebar.noFolder')) return -1;
|
||||
if (b === t('leftSidebar.noFolder')) return 1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
return folders;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {createContext, useContext, useState, useRef, type ReactNode} from 'react';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
|
||||
export interface Tab {
|
||||
id: number;
|
||||
@@ -34,15 +35,16 @@ interface TabProviderProps {
|
||||
}
|
||||
|
||||
export function TabProvider({children}: TabProviderProps) {
|
||||
const {t} = useTranslation();
|
||||
const [tabs, setTabs] = useState<Tab[]>([
|
||||
{id: 1, type: 'home', title: 'Home'}
|
||||
{id: 1, type: 'home', title: t('nav.home')}
|
||||
]);
|
||||
const [currentTab, setCurrentTab] = useState<number>(1);
|
||||
const [allSplitScreenTab, setAllSplitScreenTab] = useState<number[]>([]);
|
||||
const nextTabId = useRef(2);
|
||||
|
||||
function computeUniqueTitle(tabType: Tab['type'], desiredTitle: string | undefined): string {
|
||||
const defaultTitle = tabType === 'server' ? 'Server' : (tabType === 'file_manager' ? 'File Manager' : 'Terminal');
|
||||
const defaultTitle = tabType === 'server' ? t('nav.serverStats') : (tabType === 'file_manager' ? t('nav.fileManager') : t('nav.terminal'));
|
||||
const baseTitle = (desiredTitle || defaultTitle).trim();
|
||||
const match = baseTitle.match(/^(.*) \((\d+)\)$/);
|
||||
const root = match ? match[1] : baseTitle;
|
||||
|
||||
Reference in New Issue
Block a user