From d8f071bb15e7fdd55635f800cd6d223b5c5a6d0b Mon Sep 17 00:00:00 2001 From: LukeGus Date: Tue, 2 Sep 2025 22:44:19 -0500 Subject: [PATCH] Translation update --- README.md | 1 + src/backend/database/routes/users.ts | 63 ++--- src/ui/Admin/AdminSettings.tsx | 141 ++++++------ .../Host Manager/HostManagerHostEditor.tsx | 198 ++++++++-------- .../Host Manager/HostManagerHostViewer.tsx | 62 ++--- src/ui/Homepage/HomepageAuth.tsx | 136 +++++------ src/ui/Homepage/HompageUpdateLog.tsx | 14 +- src/ui/Navigation/LeftSidebar.tsx | 215 +++--------------- src/ui/User/PasswordReset.tsx | 44 ++-- 9 files changed, 360 insertions(+), 514 deletions(-) diff --git a/README.md b/README.md index 72d47b80..64fcca64 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Termix is an open-source, forever-free, self-hosted all-in-one server management - **Server Stats** - View CPU, memory, and HDD usage on any SSH server - **User Authentication** - Secure user management with admin controls and OIDC and 2FA (TOTP) support - **Modern UI** - Clean interface built with React, Tailwind CSS, and Shadcn +- **Languages** - Built-in support for English and Chinese # Planned Features - **Improved Admin Control** - Give more fine-grained control over user and admin permissions, share hosts, etc diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index e976da89..f0a892e8 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -1,6 +1,6 @@ import express from 'express'; import {db} from '../db/index.js'; -import {users, settings} from '../db/schema.js'; +import {users, settings, sshData, fileManagerRecent, fileManagerPinned, fileManagerShortcuts, dismissedAlerts} from '../db/schema.js'; import {eq, and} from 'drizzle-orm'; import chalk from 'chalk'; import bcrypt from 'bcryptjs'; @@ -377,10 +377,10 @@ router.get('/oidc/callback', async (req, res) => { let userInfo: any = null; let userInfoUrls: string[] = []; - + const normalizedIssuerUrl = config.issuer_url.endsWith('/') ? config.issuer_url.slice(0, -1) : config.issuer_url; const baseUrl = normalizedIssuerUrl.replace(/\/application\/o\/[^\/]+$/, ''); - + try { const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`; const discoveryResponse = await fetch(discoveryUrl); @@ -464,17 +464,17 @@ router.get('/oidc/callback', async (req, res) => { return path.split('.').reduce((current, key) => current?.[key], obj); }; - const identifier = getNestedValue(userInfo, config.identifier_path) || - userInfo[config.identifier_path] || - userInfo.sub || - userInfo.email || - userInfo.preferred_username; - - const name = getNestedValue(userInfo, config.name_path) || - userInfo[config.name_path] || - userInfo.name || - userInfo.given_name || - identifier; + const identifier = getNestedValue(userInfo, config.identifier_path) || + userInfo[config.identifier_path] || + userInfo.sub || + userInfo.email || + userInfo.preferred_username; + + const name = getNestedValue(userInfo, config.name_path) || + userInfo[config.name_path] || + userInfo.name || + userInfo.given_name || + identifier; if (!identifier) { logger.error(`Identifier not found at path: ${config.identifier_path}`); @@ -1007,7 +1007,7 @@ router.post('/totp/verify-login', async (req, res) => { } const jwtSecret = process.env.JWT_SECRET || 'secret'; - + try { const decoded = jwt.verify(temp_token, jwtSecret) as any; if (!decoded.pending_totp) { @@ -1020,7 +1020,7 @@ router.post('/totp/verify-login', async (req, res) => { } const userRecord = user[0]; - + if (!userRecord.totp_enabled || !userRecord.totp_secret) { return res.status(400).json({error: 'TOTP not enabled for this user'}); } @@ -1035,11 +1035,11 @@ router.post('/totp/verify-login', async (req, res) => { if (!verified) { const backupCodes = userRecord.totp_backup_codes ? JSON.parse(userRecord.totp_backup_codes) : []; const backupIndex = backupCodes.indexOf(totp_code); - + if (backupIndex === -1) { return res.status(401).json({error: 'Invalid TOTP code'}); } - + backupCodes.splice(backupIndex, 1); await db.update(users) .set({totp_backup_codes: JSON.stringify(backupCodes)}) @@ -1066,7 +1066,7 @@ router.post('/totp/verify-login', async (req, res) => { // POST /users/totp/setup router.post('/totp/setup', authenticateJWT, async (req, res) => { const userId = (req as any).userId; - + try { const user = await db.select().from(users).where(eq(users.id, userId)); if (!user || user.length === 0) { @@ -1074,7 +1074,7 @@ router.post('/totp/setup', authenticateJWT, async (req, res) => { } const userRecord = user[0]; - + if (userRecord.totp_enabled) { return res.status(400).json({error: 'TOTP is already enabled'}); } @@ -1118,7 +1118,7 @@ router.post('/totp/enable', authenticateJWT, async (req, res) => { } const userRecord = user[0]; - + if (userRecord.totp_enabled) { return res.status(400).json({error: 'TOTP is already enabled'}); } @@ -1138,7 +1138,7 @@ router.post('/totp/enable', authenticateJWT, async (req, res) => { return res.status(401).json({error: 'Invalid TOTP code'}); } - const backupCodes = Array.from({length: 8}, () => + const backupCodes = Array.from({length: 8}, () => Math.random().toString(36).substring(2, 10).toUpperCase() ); @@ -1177,7 +1177,7 @@ router.post('/totp/disable', authenticateJWT, async (req, res) => { } const userRecord = user[0]; - + if (!userRecord.totp_enabled) { return res.status(400).json({error: 'TOTP is not enabled'}); } @@ -1235,7 +1235,7 @@ router.post('/totp/backup-codes', authenticateJWT, async (req, res) => { } const userRecord = user[0]; - + if (!userRecord.totp_enabled) { return res.status(400).json({error: 'TOTP is not enabled'}); } @@ -1260,7 +1260,7 @@ router.post('/totp/backup-codes', authenticateJWT, async (req, res) => { return res.status(400).json({error: 'Authentication required'}); } - const backupCodes = Array.from({length: 8}, () => + const backupCodes = Array.from({length: 8}, () => Math.random().toString(36).substring(2, 10).toUpperCase() ); @@ -1311,10 +1311,15 @@ router.delete('/delete-user', authenticateJWT, async (req, res) => { const targetUserId = targetUser[0].id; try { - db.$client.prepare('DELETE FROM file_manager_recent WHERE user_id = ?').run(targetUserId); - db.$client.prepare('DELETE FROM file_manager_pinned WHERE user_id = ?').run(targetUserId); - db.$client.prepare('DELETE FROM file_manager_shortcuts WHERE user_id = ?').run(targetUserId); - db.$client.prepare('DELETE FROM ssh_data WHERE user_id = ?').run(targetUserId); + 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); } diff --git a/src/ui/Admin/AdminSettings.tsx b/src/ui/Admin/AdminSettings.tsx index dcd1270f..ca1ab4f0 100644 --- a/src/ui/Admin/AdminSettings.tsx +++ b/src/ui/Admin/AdminSettings.tsx @@ -17,15 +17,16 @@ import { } from "@/components/ui/table.tsx"; import {Shield, Trash2, Users} from "lucide-react"; import {toast} from "sonner"; -import { - getOIDCConfig, - getRegistrationAllowed, - getUserList, - updateRegistrationAllowed, - updateOIDCConfig, - makeUserAdmin, - removeAdminStatus, - deleteUser +import {useTranslation} from "react-i18next"; +import { + getOIDCConfig, + getRegistrationAllowed, + getUserList, + updateRegistrationAllowed, + updateOIDCConfig, + makeUserAdmin, + removeAdminStatus, + deleteUser } from "@/ui/main-axios.ts"; function getCookie(name: string) { @@ -40,6 +41,7 @@ interface AdminSettingsProps { } export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.ReactElement { + const {t} = useTranslation(); const {state: sidebarState} = useSidebar(); const [allowRegistration, setAllowRegistration] = React.useState(true); @@ -124,7 +126,7 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React. const required = ['client_id', 'client_secret', 'issuer_url', 'authorization_url', 'token_url']; const missing = required.filter(f => !oidcConfig[f as keyof typeof oidcConfig]); if (missing.length > 0) { - setOidcError(`Missing required fields: ${missing.join(', ')}`); + setOidcError(t('admin.missingRequiredFields', { fields: missing.join(', ') })); setOidcLoading(false); return; } @@ -132,9 +134,9 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React. const jwt = getCookie("jwt"); try { await updateOIDCConfig(oidcConfig); - toast.success("OIDC configuration updated successfully!"); + toast.success(t('admin.oidcConfigurationUpdated')); } catch (err: any) { - setOidcError(err?.response?.data?.error || "Failed to update OIDC configuration"); + setOidcError(err?.response?.data?.error || t('admin.failedToUpdateOidcConfig')); } finally { setOidcLoading(false); } @@ -152,39 +154,39 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React. const jwt = getCookie("jwt"); try { await makeUserAdmin(newAdminUsername.trim()); - toast.success(`User ${newAdminUsername} is now an admin`); + toast.success(t('admin.userIsNowAdmin', { username: newAdminUsername })); setNewAdminUsername(""); fetchUsers(); } catch (err: any) { - setMakeAdminError(err?.response?.data?.error || "Failed to make user admin"); + setMakeAdminError(err?.response?.data?.error || t('admin.failedToMakeUserAdmin')); } finally { setMakeAdminLoading(false); } }; const handleRemoveAdminStatus = async (username: string) => { - if (!confirm(`Remove admin status from ${username}?`)) return; + if (!confirm(t('admin.removeAdminStatus', { username }))) return; const jwt = getCookie("jwt"); try { await removeAdminStatus(username); - toast.success(`Admin status removed from ${username}`); + toast.success(t('admin.adminStatusRemoved', { username })); fetchUsers(); } catch (err: any) { console.error('Failed to remove admin status:', err); - toast.error('Failed to remove admin status'); + toast.error(t('admin.failedToRemoveAdminStatus')); } }; const handleDeleteUser = async (username: string) => { - if (!confirm(`Delete user ${username}? This cannot be undone.`)) return; + if (!confirm(t('admin.deleteUser', { username }))) return; const jwt = getCookie("jwt"); try { await deleteUser(username); - toast.success(`User ${username} deleted successfully`); + toast.success(t('admin.userDeletedSuccessfully', { username })); fetchUsers(); } catch (err: any) { console.error('Failed to delete user:', err); - toast.error('Failed to delete user'); + toast.error(t('admin.failedToDeleteUser')); } }; @@ -204,7 +206,7 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React. className="bg-[#18181b] text-white rounded-lg border-2 border-[#303032] overflow-hidden">
-

Admin Settings

+

{t('admin.title')}

@@ -213,7 +215,7 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React. - General + {t('admin.general')} @@ -221,97 +223,96 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React. - Users + {t('admin.users')} - Admins + {t('admin.adminManagement')}
-

User Registration

+

{t('admin.userRegistration')}

-

External Authentication (OIDC)

-

Configure external identity provider for - OIDC/OAuth2 authentication.

+

{t('admin.externalAuthentication')}

+

{t('admin.configureExternalProvider')}

{oidcError && ( - Error + {t('common.error')} {oidcError} )}
- + handleOIDCConfigChange('client_id', e.target.value)} - placeholder="your-client-id" required/> + placeholder={t('placeholders.clientId')} required/>
- + handleOIDCConfigChange('client_secret', e.target.value)} - placeholder="your-client-secret" required/> + placeholder={t('placeholders.clientSecret')} required/>
- + handleOIDCConfigChange('authorization_url', e.target.value)} - placeholder="https://your-provider.com/application/o/authorize/" + placeholder={t('placeholders.authUrl')} required/>
- + handleOIDCConfigChange('issuer_url', e.target.value)} - placeholder="https://your-provider.com/application/o/termix/" required/> + placeholder={t('placeholders.redirectUrl')} required/>
- + handleOIDCConfigChange('token_url', e.target.value)} - placeholder="https://your-provider.com/application/o/token/" required/> + placeholder={t('placeholders.tokenUrl')} required/>
- + handleOIDCConfigChange('identifier_path', e.target.value)} - placeholder="sub" required/> + placeholder={t('placeholders.userIdField')} required/>
- + handleOIDCConfigChange('name_path', e.target.value)} - placeholder="name" required/> + placeholder={t('placeholders.usernameField')} required/>
- + handleOIDCConfigChange('scopes', e.target.value)} - placeholder="openid email profile" required/> + placeholder={t('placeholders.scopes')} required/>
- + handleOIDCConfigChange('userinfo_url', e.target.value)} placeholder="https://your-provider.com/application/o/userinfo/"/>
+ disabled={oidcLoading}>{oidcLoading ? t('admin.saving') : t('admin.saveConfiguration')} + })}>{t('admin.reset')}
@@ -331,20 +332,20 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
-

User Management

+

{t('admin.userManagement')}

+ size="sm">{usersLoading ? t('admin.loading') : t('admin.refresh')}
{usersLoading ? ( -
Loading users...
+
{t('admin.loadingUsers')}
) : (
- Username - Type - Actions + {t('admin.username')} + {t('admin.type')} + {t('admin.actions')} @@ -354,11 +355,11 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React. {user.username} {user.is_admin && ( Admin + className="ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-muted/50 text-muted-foreground border border-border">{t('admin.adminBadge')} )} {user.is_oidc ? "External" : "Local"} + className="px-4">{user.is_oidc ? t('admin.external') : t('admin.local')} + disabled={makeAdminLoading || !newAdminUsername.trim()}>{makeAdminLoading ? t('admin.adding') : t('admin.makeAdmin')} {makeAdminError && ( - Error + {t('common.error')} {makeAdminError} )} @@ -403,14 +404,14 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
-

Current Admins

+

{t('admin.currentAdmins')}

- Username - Type - Actions + {t('admin.username')} + {t('admin.type')} + {t('admin.actions')} @@ -419,16 +420,16 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React. {admin.username} Admin + className="ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-muted/50 text-muted-foreground border border-border">{t('admin.adminBadge')} {admin.is_oidc ? "External" : "Local"} + className="px-4">{admin.is_oidc ? t('admin.external') : t('admin.local')} diff --git a/src/ui/Apps/Host Manager/HostManagerHostEditor.tsx b/src/ui/Apps/Host Manager/HostManagerHostEditor.tsx index e4e4ccd1..a00a15f0 100644 --- a/src/ui/Apps/Host Manager/HostManagerHostEditor.tsx +++ b/src/ui/Apps/Host Manager/HostManagerHostEditor.tsx @@ -21,6 +21,7 @@ import {Switch} from "@/components/ui/switch.tsx"; import {Alert, AlertDescription} from "@/components/ui/alert.tsx"; import {toast} from "sonner"; import {createSSHHost, updateSSHHost, getSSHHosts} from '@/ui/main-axios.ts'; +import {useTranslation} from "react-i18next"; interface SSHHost { id: number; @@ -51,6 +52,7 @@ interface SSHManagerHostEditorProps { } export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHostEditorProps) { + const {t} = useTranslation(); const [hosts, setHosts] = useState([]); const [folders, setFolders] = useState([]); const [sshConfigurations, setSshConfigurations] = useState([]); @@ -128,7 +130,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos if (!data.password || data.password.trim() === '') { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: "Password is required when using password authentication", + message: t('hosts.passwordRequired'), path: ['password'] }); } @@ -136,14 +138,14 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos if (!data.key) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: "SSH Private Key is required when using key authentication", + message: t('hosts.sshKeyRequired'), path: ['key'] }); } if (!data.keyType) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: "Key Type is required when using key authentication", + message: t('hosts.keyTypeRequired'), path: ['keyType'] }); } @@ -153,7 +155,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos if (connection.endpointHost && !sshConfigurations.includes(connection.endpointHost)) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: "Must select a valid SSH configuration from the list", + message: t('hosts.mustSelectValidSshConfig'), path: ['tunnelConnections', index, 'endpointHost'] }); } @@ -245,10 +247,10 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos if (editingHost) { await updateSSHHost(editingHost.id, formData); - toast.success(`Host "${formData.name}" updated successfully!`); + toast.success(t('hosts.hostUpdatedSuccessfully', { name: formData.name })); } else { await createSSHHost(formData); - toast.success(`Host "${formData.name}" added successfully!`); + toast.success(t('hosts.hostAddedSuccessfully', { name: formData.name })); } if (onFormSubmit) { @@ -257,7 +259,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos window.dispatchEvent(new CustomEvent('ssh-hosts:changed')); } catch (error) { - toast.error('Failed to save host. Please try again.'); + toast.error(t('hosts.failedToSaveHost')); } }; @@ -302,15 +304,15 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos }, [folderDropdownOpen]); const keyTypeOptions = [ - {value: 'auto', label: 'Auto-detect'}, - {value: 'ssh-rsa', label: 'RSA'}, - {value: 'ssh-ed25519', label: 'ED25519'}, - {value: 'ecdsa-sha2-nistp256', label: 'ECDSA NIST P-256'}, - {value: 'ecdsa-sha2-nistp384', label: 'ECDSA NIST P-384'}, - {value: 'ecdsa-sha2-nistp521', label: 'ECDSA NIST P-521'}, - {value: 'ssh-dss', label: 'DSA'}, - {value: 'ssh-rsa-sha2-256', label: 'RSA SHA2-256'}, - {value: 'ssh-rsa-sha2-512', label: 'RSA SHA2-512'}, + {value: 'auto', label: t('hosts.autoDetect')}, + {value: 'ssh-rsa', label: t('hosts.rsa')}, + {value: 'ssh-ed25519', label: t('hosts.ed25519')}, + {value: 'ecdsa-sha2-nistp256', label: t('hosts.ecdsaNistP256')}, + {value: 'ecdsa-sha2-nistp384', label: t('hosts.ecdsaNistP384')}, + {value: 'ecdsa-sha2-nistp521', label: t('hosts.ecdsaNistP521')}, + {value: 'ssh-dss', label: t('hosts.dsa')}, + {value: 'ssh-rsa-sha2-256', label: t('hosts.rsaSha2256')}, + {value: 'ssh-rsa-sha2-512', label: t('hosts.rsaSha2512')}, ]; const [keyTypeDropdownOpen, setKeyTypeDropdownOpen] = useState(false); @@ -396,22 +398,22 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos - General - Terminal - Tunnel - File Manager + {t('hosts.general')} + {t('hosts.terminal')} + {t('hosts.tunnel')} + {t('hosts.fileManager')} - Connection Details + {t('hosts.connectionDetails')}
( - IP + {t('hosts.ipAddress')} - + )} @@ -422,9 +424,9 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos name="port" render={({field}) => ( - Port + {t('hosts.port')} - + )} @@ -435,24 +437,24 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos name="username" render={({field}) => ( - Username + {t('hosts.username')} - + )} />
- Organization + {t('hosts.organization')}
( - Name + {t('hosts.name')} - + )} @@ -463,11 +465,11 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos name="folder" render={({field}) => ( - Folder + {t('hosts.folder')} ( - Tags + {t('hosts.tags')}
@@ -544,7 +546,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos field.onChange(field.value.slice(0, -1)); } }} - placeholder="add tags (space to add)" + placeholder={t('hosts.addTagsSpaceToAdd')} />
@@ -557,7 +559,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos name="pin" render={({field}) => ( - Pin Connection + {t('hosts.pin')}
- Authentication + {t('hosts.authentication')} { @@ -578,8 +580,8 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos className="flex-1 flex flex-col h-full min-h-0" > - Password - Key + {t('hosts.password')} + {t('hosts.key')} ( - Password + {t('hosts.password')} - + )} @@ -602,7 +604,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos name="key" render={({field}) => ( - SSH Private Key + {t('hosts.sshPrivateKey')}
- {field.value ? (editingHost ? 'Update Key' : field.value.name) : 'Upload'} + title={field.value?.name || t('hosts.upload')}> + {field.value ? (editingHost ? t('hosts.updateKey') : field.value.name) : t('hosts.upload')}
@@ -635,10 +637,10 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos name="keyPassword" render={({field}) => ( - Key Password + {t('hosts.keyPassword')} @@ -651,7 +653,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos name="keyType" render={({field}) => ( - Key Type + {t('hosts.keyType')}
{keyTypeDropdownOpen && (
( - Enable Terminal + {t('hosts.enableTerminal')} - Enable/disable host visibility in Terminal tab. + {t('hosts.enableTerminalDesc')} )} @@ -722,7 +724,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos name="enableTunnel" render={({field}) => ( - Enable Tunnel + {t('hosts.enableTunnel')} - Enable/disable host visibility in Tunnel tab. + {t('hosts.enableTunnelDesc')} )} @@ -740,44 +742,40 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos <> - Sshpass Required For Password Authentication + {t('hosts.sshpassRequired')}
- For password-based SSH authentication, sshpass must be installed on - both the local and remote servers. Install with: sudo apt install sshpass (Debian/Ubuntu) or the equivalent for your OS.
- Other installation methods: -
• CentOS/RHEL/Fedora: {t('hosts.otherInstallMethods')} +
• {t('hosts.centosRhelFedora')} sudo yum install sshpass or sudo dnf install sshpass
-
• macOS: brew +
• {t('hosts.macos')} brew install hudochenkov/sshpass/sshpass
-
• Windows: Use WSL or consider SSH key authentication
+
• {t('hosts.windows')}
- SSH Server Configuration Required -
For reverse SSH tunnels, the endpoint SSH server must allow:
+ {t('hosts.sshServerConfigRequired')} +
{t('hosts.sshServerConfigDesc')}
GatewayPorts - yes (bind remote ports) + yes {t('hosts.gatewayPortsYes')}
AllowTcpForwarding - yes (port forwarding) + yes {t('hosts.allowTcpForwardingYes')}
PermitRootLogin - yes (if using root) + yes {t('hosts.permitRootLoginYes')}
-
Edit /etc/ssh/sshd_config and - restart SSH: sudo - systemctl restart sshd
+
{t('hosts.editSshConfig')}
@@ -786,7 +784,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos name="tunnelConnections" render={({field}) => ( - Tunnel Connections + {t('hosts.tunnelConnections')}
{field.value.map((connection, index) => ( @@ -794,7 +792,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos className="p-4 border rounded-lg bg-muted/50">
-

Connection {index + 1}

+

{t('hosts.connection')} {index + 1}

@@ -813,10 +811,8 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos name={`tunnelConnections.${index}.sourcePort`} render={({field: sourcePortField}) => ( - Source Port - (Source refers to the Current - Connection Details in the - General tab) + {t('hosts.sourcePort')} + {t('hosts.sourcePortDesc')} @@ -829,8 +825,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos name={`tunnelConnections.${index}.endpointPort`} render={({field: endpointPortField}) => ( - Endpoint Port - (Remote) + {t('hosts.endpointPort')} @@ -844,14 +839,13 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos render={({field: endpointHostField}) => ( - Endpoint SSH - Configuration + {t('hosts.endpointSshConfig')} { sshConfigInputRefs.current[index] = el; }} - placeholder="endpoint ssh configuration" + placeholder={t('placeholders.sshConfig')} className="min-h-[40px]" autoComplete="off" value={endpointHostField.value} @@ -898,12 +892,10 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos

- This tunnel will forward traffic from - port {form.watch(`tunnelConnections.${index}.sourcePort`) || '22'} on - the source machine (current connection details - in general tab) to - port {form.watch(`tunnelConnections.${index}.endpointPort`) || '224'} on - the endpoint machine. + {t('hosts.tunnelForwardDescription', { + sourcePort: form.watch(`tunnelConnections.${index}.sourcePort`) || '22', + endpointPort: form.watch(`tunnelConnections.${index}.endpointPort`) || '224' + })}

@@ -912,14 +904,13 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos name={`tunnelConnections.${index}.maxRetries`} render={({field: maxRetriesField}) => ( - Max Retries + {t('hosts.maxRetries')} + placeholder={t('placeholders.maxRetries')} {...maxRetriesField} /> - Maximum number of retry attempts - for tunnel connection. + {t('hosts.maxRetriesDescription')} )} @@ -929,15 +920,13 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos name={`tunnelConnections.${index}.retryInterval`} render={({field: retryIntervalField}) => ( - Retry Interval - (seconds) + {t('hosts.retryInterval')} + placeholder={t('placeholders.retryInterval')} {...retryIntervalField} /> - Time to wait between retry - attempts. + {t('hosts.retryIntervalDescription')} )} @@ -947,8 +936,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos name={`tunnelConnections.${index}.autoStart`} render={({field}) => ( - Auto Start on Container - Launch + {t('hosts.autoStartContainer')} - Automatically start this tunnel - when the container launches. + {t('hosts.autoStartDesc')} )} @@ -979,7 +966,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos }]); }} > - Add Tunnel Connection + {t('hosts.addConnection')}
@@ -997,7 +984,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos name="enableFileManager" render={({field}) => ( - Enable File Manager + {t('hosts.enableFileManager')} - Enable/disable host visibility in File Manager tab. + {t('hosts.enableFileManagerDesc')} )} @@ -1018,12 +1005,11 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos name="defaultPath" render={({field}) => ( - Default Path + {t('hosts.defaultPath')} - + - Set default directory shown when connected via - File Manager + {t('hosts.defaultPathDesc')} )} /> @@ -1042,7 +1028,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos transform: 'translateY(8px)' }} > - {editingHost ? "Update Host" : "Add Host"} + {editingHost ? t('hosts.updateHost') : t('hosts.addHost')} diff --git a/src/ui/Apps/Host Manager/HostManagerHostViewer.tsx b/src/ui/Apps/Host Manager/HostManagerHostViewer.tsx index 61942489..33574649 100644 --- a/src/ui/Apps/Host Manager/HostManagerHostViewer.tsx +++ b/src/ui/Apps/Host Manager/HostManagerHostViewer.tsx @@ -8,6 +8,7 @@ import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/co import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip"; import {getSSHHosts, deleteSSHHost, bulkImportSSHHosts} from "@/ui/main-axios.ts"; import {toast} from "sonner"; +import {useTranslation} from "react-i18next"; import { Edit, Trash2, @@ -48,6 +49,7 @@ interface SSHManagerHostViewerProps { } export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) { + const {t} = useTranslation(); const [hosts, setHosts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -65,21 +67,21 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) { setHosts(data); setError(null); } catch (err) { - setError('Failed to load hosts'); + setError(t('hosts.failedToLoadHosts')); } finally { setLoading(false); } }; const handleDelete = async (hostId: number, hostName: string) => { - if (window.confirm(`Are you sure you want to delete "${hostName}"?`)) { + if (window.confirm(t('hosts.confirmDelete', { name: hostName }))) { try { await deleteSSHHost(hostId); - toast.success(`Host "${hostName}" deleted successfully!`); + toast.success(t('hosts.hostDeletedSuccessfully', { name: hostName })); await fetchHosts(); window.dispatchEvent(new CustomEvent('ssh-hosts:changed')); } catch (err) { - toast.error('Failed to delete host'); + toast.error(t('hosts.failedToDeleteHost')); } } }; @@ -100,35 +102,35 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) { const data = JSON.parse(text); if (!Array.isArray(data.hosts) && !Array.isArray(data)) { - throw new Error('JSON must contain a "hosts" array or be an array of hosts'); + throw new Error(t('hosts.jsonMustContainHosts')); } const hostsArray = Array.isArray(data.hosts) ? data.hosts : data; if (hostsArray.length === 0) { - throw new Error('No hosts found in JSON file'); + throw new Error(t('hosts.noHostsInJson')); } if (hostsArray.length > 100) { - throw new Error('Maximum 100 hosts allowed per import'); + throw new Error(t('hosts.maxHostsAllowed')); } const result = await bulkImportSSHHosts(hostsArray); if (result.success > 0) { - toast.success(`Import completed: ${result.success} hosts imported successfully${result.failed > 0 ? `, ${result.failed} failed` : ''}`); + toast.success(t('hosts.importCompleted', { success: result.success, failed: result.failed })); if (result.errors.length > 0) { toast.error(`Import errors: ${result.errors.join(', ')}`); } await fetchHosts(); window.dispatchEvent(new CustomEvent('ssh-hosts:changed')); } else { - toast.error(`Import failed: ${result.errors.join(', ')}`); + toast.error(t('hosts.importFailed') + `: ${result.errors.join(', ')}`); } } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Failed to import JSON file'; - toast.error(`Import error: ${errorMessage}`); + const errorMessage = err instanceof Error ? err.message : t('hosts.failedToImportJson'); + toast.error(t('hosts.importError') + `: ${errorMessage}`); } finally { setImporting(false); event.target.value = ''; @@ -168,7 +170,7 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) { const grouped: { [key: string]: SSHHost[] } = {}; filteredAndSortedHosts.forEach(host => { - const folder = host.folder || 'Uncategorized'; + const folder = host.folder || t('hosts.uncategorized'); if (!grouped[folder]) { grouped[folder] = []; } @@ -176,8 +178,8 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) { }); const sortedFolders = Object.keys(grouped).sort((a, b) => { - if (a === 'Uncategorized') return -1; - if (b === 'Uncategorized') return 1; + if (a === t('hosts.uncategorized')) return -1; + if (b === t('hosts.uncategorized')) return 1; return a.localeCompare(b); }); @@ -194,7 +196,7 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
-

Loading hosts...

+

{t('hosts.loadingHosts')}

); @@ -206,7 +208,7 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {

{error}

@@ -218,9 +220,9 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
-

No SSH Hosts

+

{t('hosts.noHosts')}

- You haven't added any SSH hosts yet. Click "Add Host" to get started. + {t('hosts.noHostsMessage')}

@@ -231,9 +233,9 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
-

SSH Hosts

+

{t('hosts.sshHosts')}

- {filteredAndSortedHosts.length} hosts + {t('hosts.hostsCount', { count: filteredAndSortedHosts.length })}

@@ -247,15 +249,15 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) { onClick={() => document.getElementById('json-import-input')?.click()} disabled={importing} > - {importing ? 'Importing...' : 'Import JSON'} + {importing ? t('hosts.importing') : t('hosts.importJson')}
-

Import SSH Hosts from JSON

+

{t('hosts.importJsonTitle')}

- Upload a JSON file to bulk import multiple SSH hosts (max 100). + {t('hosts.importJsonDesc')}

@@ -323,7 +325,7 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) { URL.revokeObjectURL(url); }} > - Download Sample + {t('hosts.downloadSample')}
@@ -355,7 +357,7 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
setSearchQuery(e.target.value)} className="pl-10" @@ -451,13 +453,13 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) { {host.enableTerminal && ( - Terminal + {t('hosts.terminalBadge')} )} {host.enableTunnel && ( - Tunnel + {t('hosts.tunnelBadge')} {host.tunnelConnections && host.tunnelConnections.length > 0 && ( ({host.tunnelConnections.length}) @@ -467,7 +469,7 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) { {host.enableFileManager && ( - File Manager + {t('hosts.fileManagerBadge')} )}
diff --git a/src/ui/Homepage/HomepageAuth.tsx b/src/ui/Homepage/HomepageAuth.tsx index 51212b33..5b1ca837 100644 --- a/src/ui/Homepage/HomepageAuth.tsx +++ b/src/ui/Homepage/HomepageAuth.tsx @@ -4,6 +4,8 @@ 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 {LanguageSwitcher} from "../../components/LanguageSwitcher"; import { registerUser, loginUser, @@ -55,6 +57,7 @@ export function HomepageAuth({ onAuthSuccess, ...props }: HomepageAuthProps) { + const {t} = useTranslation(); const [tab, setTab] = useState<"login" | "signup" | "external" | "reset">("login"); const [localUsername, setLocalUsername] = useState(""); const [password, setPassword] = useState(""); @@ -116,7 +119,7 @@ export function HomepageAuth({ } setDbError(null); }).catch(() => { - setDbError("Could not connect to the database. Please try again later."); + setDbError(t('errors.databaseConnection')); }); }, [setDbError]); @@ -126,7 +129,7 @@ export function HomepageAuth({ setLoading(true); if (!localUsername.trim()) { - setError("Username is required"); + setError(t('errors.requiredField')); setLoading(false); return; } @@ -137,12 +140,12 @@ export function HomepageAuth({ res = await loginUser(localUsername, password); } else { if (password !== signupConfirmPassword) { - setError("Passwords do not match"); + setError(t('errors.passwordMismatch')); setLoading(false); return; } if (password.length < 6) { - setError("Password must be at least 6 characters long"); + setError(t('errors.minLength', {min: 6})); setLoading(false); return; } @@ -159,7 +162,7 @@ export function HomepageAuth({ } if (!res || !res.token) { - throw new Error('No token received from login'); + throw new Error(t('errors.noTokenReceived')); } setCookie("jwt", res.token); @@ -186,7 +189,7 @@ export function HomepageAuth({ setTotpCode(""); setTotpTempToken(""); } catch (err: any) { - setError(err?.response?.data?.error || err?.message || "Unknown error"); + setError(err?.response?.data?.error || err?.message || t('errors.unknownError')); setInternalLoggedIn(false); setLoggedIn(false); setIsAdmin(false); @@ -194,7 +197,7 @@ export function HomepageAuth({ setUserId(null); setCookie("jwt", "", -1); if (err?.response?.data?.error?.includes("Database")) { - setDbError("Could not connect to the database. Please try again later."); + setDbError(t('errors.databaseConnection')); } else { setDbError(null); } @@ -211,7 +214,7 @@ export function HomepageAuth({ setResetStep("verify"); setError(null); } catch (err: any) { - setError(err?.response?.data?.error || err?.message || "Failed to initiate password reset"); + setError(err?.response?.data?.error || err?.message || t('errors.failedPasswordReset')); } finally { setResetLoading(false); } @@ -226,7 +229,7 @@ export function HomepageAuth({ setResetStep("newPassword"); setError(null); } catch (err: any) { - setError(err?.response?.data?.error || "Failed to verify reset code"); + setError(err?.response?.data?.error || t('errors.failedVerifyCode')); } finally { setResetLoading(false); } @@ -237,13 +240,13 @@ export function HomepageAuth({ setResetLoading(true); if (newPassword !== confirmPassword) { - setError("Passwords do not match"); + setError(t('errors.passwordMismatch')); setResetLoading(false); return; } if (newPassword.length < 6) { - setError("Password must be at least 6 characters long"); + setError(t('errors.minLength', {min: 6})); setResetLoading(false); return; } @@ -260,7 +263,7 @@ export function HomepageAuth({ setResetSuccess(true); } catch (err: any) { - setError(err?.response?.data?.error || "Failed to complete password reset"); + setError(err?.response?.data?.error || t('errors.failedCompleteReset')); } finally { setResetLoading(false); } @@ -285,7 +288,7 @@ export function HomepageAuth({ async function handleTOTPVerification() { if (totpCode.length !== 6) { - setError("Please enter a 6-digit code"); + setError(t('auth.enterCode')); return; } @@ -296,7 +299,7 @@ export function HomepageAuth({ const res = await verifyTOTPLogin(totpTempToken, totpCode); if (!res || !res.token) { - throw new Error('No token received from TOTP verification'); + throw new Error(t('errors.noTokenReceived')); } setCookie("jwt", res.token); @@ -318,7 +321,7 @@ export function HomepageAuth({ setTotpCode(""); setTotpTempToken(""); } catch (err: any) { - setError(err?.response?.data?.error || err?.message || "Invalid TOTP code"); + setError(err?.response?.data?.error || err?.message || t('errors.invalidTotpCode')); } finally { setTotpLoading(false); } @@ -332,12 +335,12 @@ export function HomepageAuth({ const {auth_url: authUrl} = authResponse; if (!authUrl || authUrl === 'undefined') { - throw new Error('Invalid authorization URL received from backend'); + throw new Error(t('errors.invalidAuthUrl')); } window.location.replace(authUrl); } catch (err: any) { - setError(err?.response?.data?.error || err?.message || "Failed to start OIDC login"); + setError(err?.response?.data?.error || err?.message || t('errors.failedOidcLogin')); setOidcLoading(false); } } @@ -349,7 +352,7 @@ export function HomepageAuth({ const error = urlParams.get('error'); if (error) { - setError(`OIDC authentication failed: ${error}`); + setError(`${t('errors.oidcAuthFailed')}: ${error}`); setOidcLoading(false); window.history.replaceState({}, document.title, window.location.pathname); return; @@ -377,7 +380,7 @@ export function HomepageAuth({ window.history.replaceState({}, document.title, window.location.pathname); }) .catch(err => { - setError("Failed to get user info after OIDC login"); + setError(t('errors.failedUserInfo')); setInternalLoggedIn(false); setLoggedIn(false); setIsAdmin(false); @@ -412,39 +415,37 @@ export function HomepageAuth({ )} {firstUser && !dbError && !internalLoggedIn && ( - First User + {t('auth.firstUser')} - You are the first user and will be made an admin. You can view admin settings in the sidebar - user dropdown. If you think this is a mistake, check the docker logs, or create a{" "} + {t('auth.firstUserMessage')}{" "} - GitHub issue + GitHub Issue . )} {!registrationAllowed && !internalLoggedIn && ( - Registration Disabled + {t('auth.registerTitle')} - New account registration is currently disabled by an admin. Please log in or contact an - administrator. + {t('messages.registrationDisabled')} )} {totpRequired && (
-

Two-Factor Authentication

-

Enter the 6-digit code from your authenticator app

+

{t('auth.twoFactorAuth')}

+

{t('auth.enterCode')}

- +

- Or enter a backup code if you don't have access to your authenticator + {t('auth.backupCode')}

@@ -467,7 +468,7 @@ export function HomepageAuth({ disabled={totpLoading || totpCode.length < 6} onClick={handleTOTPVerification} > - {totpLoading ? Spinner : "Verify"} + {totpLoading ? Spinner : t('auth.verifyCode')}
)} @@ -506,7 +507,7 @@ export function HomepageAuth({ aria-selected={tab === "login"} disabled={loading || firstUser} > - Login + {t('common.login')} {oidcConfigured && ( )}

- {tab === "login" ? "Login to your account" : - tab === "signup" ? "Create a new account" : - tab === "external" ? "Login with external provider" : - "Reset your password"} + {tab === "login" ? t('auth.loginTitle') : + tab === "signup" ? t('auth.registerTitle') : + tab === "external" ? t('auth.loginWithExternal') : + t('auth.forgotPassword')}

@@ -561,7 +562,7 @@ export function HomepageAuth({ {tab === "external" && ( <>
-

Login using your configured external identity provider

+

{t('auth.loginWithExternalDesc')}

)} @@ -578,12 +579,11 @@ export function HomepageAuth({ {resetStep === "initiate" && ( <>
-

Enter your username to receive a password reset code. The code - will be logged in the docker container logs.

+

{t('auth.resetCodeDesc')}

- + - {resetLoading ? Spinner : "Send Reset Code"} + {resetLoading ? Spinner : t('auth.sendResetCode')}
@@ -609,12 +609,11 @@ export function HomepageAuth({ {resetStep === "verify" && ( <>o
-

Enter the 6-digit code from the docker container logs for - user: {localUsername}

+

{t('auth.enterResetCode')} {localUsername}

- + - {resetLoading ? Spinner : "Verify Code"} + {resetLoading ? Spinner : t('auth.verifyCodeButton')}
@@ -654,10 +653,9 @@ export function HomepageAuth({ {resetSuccess && ( <> - Success! + {t('auth.passwordResetSuccess')} - Your password has been successfully reset! You can now log in - with your new password. + {t('auth.passwordResetSuccessDesc')} )} @@ -676,12 +674,11 @@ export function HomepageAuth({ {resetStep === "newPassword" && !resetSuccess && ( <>
-

Enter your new password for - user: {localUsername}

+

{t('auth.enterNewPassword')} {localUsername}

- +
- + - {resetLoading ? Spinner : "Reset Password"} + {resetLoading ? Spinner : t('auth.resetPasswordButton')}
@@ -736,7 +733,7 @@ export function HomepageAuth({ ) : (
- +
- + setPassword(e.target.value)} disabled={loading || internalLoggedIn}/>
{tab === "signup" && (
- + - {loading ? Spinner : (tab === "login" ? "Login" : "Sign Up")} + {loading ? Spinner : (tab === "login" ? t('common.login') : t('auth.signUp'))} {tab === "login" && ( )} )} + +
+
+
+ +
+ +
+
)} {error && ( diff --git a/src/ui/Homepage/HompageUpdateLog.tsx b/src/ui/Homepage/HompageUpdateLog.tsx index c95f5bb0..4f8eaf09 100644 --- a/src/ui/Homepage/HompageUpdateLog.tsx +++ b/src/ui/Homepage/HompageUpdateLog.tsx @@ -69,7 +69,7 @@ export function HomepageUpdateLog({loggedIn}: HomepageUpdateLogProps) { setError(null); }) .catch(err => { - setError('Failed to fetch update information'); + setError(t('common.failedToFetchUpdateInfo')); }) .finally(() => setLoading(false)); } @@ -96,9 +96,9 @@ export function HomepageUpdateLog({loggedIn}: HomepageUpdateLogProps) { {versionInfo && versionInfo.status === 'requires_update' && ( - Update Available + {t('common.updateAvailable')} - A new version ({versionInfo.version}) is available. + {t('common.newVersionAvailable', { version: versionInfo.version })} )} @@ -117,7 +117,7 @@ export function HomepageUpdateLog({loggedIn}: HomepageUpdateLogProps) { {error && ( - Error + {t('common.error')} {error} )} @@ -135,7 +135,7 @@ export function HomepageUpdateLog({loggedIn}: HomepageUpdateLogProps) { {release.isPrerelease && ( - Pre-release + {t('common.preRelease')} )}
@@ -158,9 +158,9 @@ export function HomepageUpdateLog({loggedIn}: HomepageUpdateLogProps) { {releases && releases.items.length === 0 && !loading && ( - No Releases + {t('common.noReleases')} - No releases found. + {t('common.noReleasesFound')} )} diff --git a/src/ui/Navigation/LeftSidebar.tsx b/src/ui/Navigation/LeftSidebar.tsx index 9c769e95..1c4828b6 100644 --- a/src/ui/Navigation/LeftSidebar.tsx +++ b/src/ui/Navigation/LeftSidebar.tsx @@ -5,6 +5,7 @@ import { File, Hammer, ChevronUp, User2, HardDrive, Trash2, Users, Shield, Settings, Menu, ChevronRight } from "lucide-react"; +import { useTranslation } from 'react-i18next'; import { Sidebar, @@ -49,14 +50,7 @@ import {Card} from "@/components/ui/card.tsx"; import {FolderCard} from "@/ui/Navigation/Hosts/FolderCard.tsx"; import {getSSHHosts} from "@/ui/main-axios.ts"; import {useTabs} from "@/ui/Navigation/Tabs/TabContext.tsx"; -import { - getOIDCConfig, - getUserList, - makeUserAdmin, - removeAdminStatus, - deleteUser, - deleteAccount -} from "@/ui/main-axios.ts"; +import { deleteAccount } from "@/ui/main-axios.ts"; interface SSHHost { id: number; @@ -112,26 +106,12 @@ export function LeftSidebar({ username, children, }: SidebarProps): React.ReactElement { - const [adminSheetOpen, setAdminSheetOpen] = React.useState(false); - + const { t } = useTranslation(); + const [deleteAccountOpen, setDeleteAccountOpen] = React.useState(false); const [deletePassword, setDeletePassword] = React.useState(""); const [deleteLoading, setDeleteLoading] = React.useState(false); const [deleteError, setDeleteError] = React.useState(null); - const [adminCount, setAdminCount] = React.useState(0); - - const [users, setUsers] = React.useState>([]); - const [newAdminUsername, setNewAdminUsername] = React.useState(""); - const [usersLoading, setUsersLoading] = React.useState(false); - const [makeAdminLoading, setMakeAdminLoading] = React.useState(false); - const [makeAdminError, setMakeAdminError] = React.useState(null); - const [makeAdminSuccess, setMakeAdminSuccess] = React.useState(null); - const [oidcConfig, setOidcConfig] = React.useState(null); const [isSidebarOpen, setIsSidebarOpen] = useState(true); @@ -161,33 +141,7 @@ export function LeftSidebar({ const [search, setSearch] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState(""); - React.useEffect(() => { - if (adminSheetOpen) { - const jwt = getCookie("jwt"); - if (jwt && isAdmin) { - getOIDCConfig().then(res => { - if (res) { - setOidcConfig(res); - } - }).catch((error) => { - }); - fetchUsers(); - } - } else { - const jwt = getCookie("jwt"); - if (jwt && isAdmin) { - fetchAdminCount(); - } - } - }, [adminSheetOpen, isAdmin]); - React.useEffect(() => { - if (!isAdmin) { - setAdminSheetOpen(false); - setUsers([]); - setAdminCount(0); - } - }, [isAdmin]); const fetchHosts = React.useCallback(async () => { try { @@ -304,7 +258,7 @@ export function LeftSidebar({ setDeleteError(null); if (!deletePassword.trim()) { - setDeleteError("Password is required"); + setDeleteError(t('leftSidebar.passwordRequired')); setDeleteLoading(false); return; } @@ -315,103 +269,11 @@ export function LeftSidebar({ handleLogout(); } catch (err: any) { - setDeleteError(err?.response?.data?.error || "Failed to delete account"); + setDeleteError(err?.response?.data?.error || t('leftSidebar.failedToDeleteAccount')); setDeleteLoading(false); } }; - const fetchUsers = async () => { - const jwt = getCookie("jwt"); - - if (!jwt || !isAdmin) { - return; - } - - setUsersLoading(true); - try { - const response = await getUserList(); - setUsers(response.users); - - const adminUsers = response.users.filter((user: any) => user.is_admin); - setAdminCount(adminUsers.length); - } catch (err: any) { - } finally { - setUsersLoading(false); - } - }; - - const fetchAdminCount = async () => { - const jwt = getCookie("jwt"); - - if (!jwt || !isAdmin) { - return; - } - - try { - const response = await getUserList(); - const adminUsers = response.users.filter((user: any) => user.is_admin); - setAdminCount(adminUsers.length); - } catch (err: any) { - } - }; - - const handleMakeUserAdmin = async (e: React.FormEvent) => { - e.preventDefault(); - if (!newAdminUsername.trim()) return; - - if (!isAdmin) { - return; - } - - setMakeAdminLoading(true); - setMakeAdminError(null); - setMakeAdminSuccess(null); - - const jwt = getCookie("jwt"); - try { - await makeUserAdmin(newAdminUsername.trim()); - setMakeAdminSuccess(`User ${newAdminUsername} is now an admin`); - setNewAdminUsername(""); - fetchUsers(); - } catch (err: any) { - setMakeAdminError(err?.response?.data?.error || "Failed to make user admin"); - } finally { - setMakeAdminLoading(false); - } - }; - - const handleRemoveAdminStatus = async (username: string) => { - if (!confirm(`Are you sure you want to remove admin status from ${username}?`)) return; - - if (!isAdmin) { - return; - } - - const jwt = getCookie("jwt"); - try { - await removeAdminStatus(username); - fetchUsers(); - } catch (err: any) { - console.error('Failed to remove admin status:', err); - } - }; - - const handleDeleteUser = async (username: string) => { - if (!confirm(`Are you sure you want to delete user ${username}? This action cannot be undone.`)) return; - - if (!isAdmin) { - return; - } - - const jwt = getCookie("jwt"); - try { - await deleteUser(username); - fetchUsers(); - } catch (err: any) { - console.error('Failed to delete user:', err); - } - }; - return (
@@ -423,6 +285,7 @@ export function LeftSidebar({ variant="outline" onClick={() => setIsSidebarOpen(!isSidebarOpen)} className="w-[28px] h-[28px] absolute right-5" + title={t('common.toggleSidebar')} > @@ -433,9 +296,9 @@ export function LeftSidebar({ @@ -444,7 +307,7 @@ export function LeftSidebar({ setSearch(e.target.value)} - placeholder="Search hosts by any info..." + placeholder={t('placeholders.searchHostsAny')} className="w-full h-8 text-sm border-2 !bg-[#222225] border-[#303032] rounded-md" autoComplete="off" /> @@ -454,7 +317,7 @@ export function LeftSidebar({
- {hostsError} + {t('leftSidebar.failedToLoadHosts')}
)} @@ -462,7 +325,7 @@ export function LeftSidebar({ {hostsLoading && (
- Loading hosts... + {t('hosts.loadingHosts')}
)} @@ -489,7 +352,7 @@ export function LeftSidebar({ style={{width: '100%'}} disabled={disabled} > - {username ? username : 'Signed out'} + {username ? username : t('common.logout')} @@ -508,10 +371,10 @@ export function LeftSidebar({ setCurrentTab(profileTab.id); return; } - const id = addTab({type: 'profile', title: 'Profile'} as any); + const id = addTab({type: 'profile', title: t('profile.title')} as any); setCurrentTab(id); }}> - Profile & Security + {t('profile.title')} {isAdmin && ( { if (isAdmin) openAdminTab(); }}> - Admin Settings + {t('admin.title')} )} - Sign out + {t('common.logout')} setDeleteAccountOpen(true)} - disabled={isAdmin && adminCount <= 1} > - - Delete Account - {isAdmin && adminCount <= 1 && " (Last Admin)"} + + {t('leftSidebar.deleteAccount')} @@ -588,7 +448,7 @@ export function LeftSidebar({ onClick={(e) => e.stopPropagation()} >
-

Delete Account

+

{t('leftSidebar.deleteAccount')}

@@ -607,48 +467,33 @@ export function LeftSidebar({
- This action cannot be undone. This will permanently delete your account and all - associated data. + {t('leftSidebar.deleteAccountWarning')}
- Warning + {t('common.warning')} - Deleting your account will remove all your data including SSH hosts, - configurations, and settings. - This action is irreversible. + {t('leftSidebar.deleteAccountWarningDetails')} {deleteError && ( - Error + {t('common.error')} {deleteError} )}
- {isAdmin && adminCount <= 1 && ( - - Cannot Delete Account - - You are the last admin user. You cannot delete your account as this - would leave the system without any administrators. - Please make another user an admin first, or contact system support. - - - )} -
- + setDeletePassword(e.target.value)} - placeholder="Enter your password to confirm" + placeholder={t('placeholders.confirmPassword')} required - disabled={isAdmin && adminCount <= 1} />
@@ -657,9 +502,9 @@ export function LeftSidebar({ type="submit" variant="destructive" className="flex-1" - disabled={deleteLoading || !deletePassword.trim() || (isAdmin && adminCount <= 1)} + disabled={deleteLoading || !deletePassword.trim()} > - {deleteLoading ? "Deleting..." : "Delete Account"} + {deleteLoading ? t('leftSidebar.deleting') : t('leftSidebar.deleteAccount')}
diff --git a/src/ui/User/PasswordReset.tsx b/src/ui/User/PasswordReset.tsx index df8b8db1..ab5d9499 100644 --- a/src/ui/User/PasswordReset.tsx +++ b/src/ui/User/PasswordReset.tsx @@ -7,6 +7,7 @@ import {Input} from "@/components/ui/input.tsx"; import {Button} from "@/components/ui/button.tsx"; import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert.tsx"; import {toast} from "sonner"; +import {useTranslation} from "react-i18next"; interface PasswordResetProps { userInfo: { @@ -18,6 +19,7 @@ interface PasswordResetProps { } export function PasswordReset({userInfo}: PasswordResetProps) { + const {t} = useTranslation(); const [error, setError] = useState(null); const [resetStep, setResetStep] = useState<"initiate" | "verify" | "newPassword">("initiate"); @@ -35,7 +37,7 @@ export function PasswordReset({userInfo}: PasswordResetProps) { setResetStep("verify"); setError(null); } catch (err: any) { - setError(err?.response?.data?.error || err?.message || "Failed to initiate password reset"); + setError(err?.response?.data?.error || err?.message || t('common.failedToInitiatePasswordReset')); } finally { setResetLoading(false); } @@ -59,7 +61,7 @@ export function PasswordReset({userInfo}: PasswordResetProps) { setResetStep("newPassword"); setError(null); } catch (err: any) { - setError(err?.response?.data?.error || "Failed to verify reset code"); + setError(err?.response?.data?.error || t('common.failedToVerifyResetCode')); } finally { setResetLoading(false); } @@ -70,13 +72,13 @@ export function PasswordReset({userInfo}: PasswordResetProps) { setResetLoading(true); if (newPassword !== confirmPassword) { - setError("Passwords do not match"); + setError(t('common.passwordsDoNotMatch')); setResetLoading(false); return; } if (newPassword.length < 6) { - setError("Password must be at least 6 characters long"); + setError(t('common.passwordMinLength')); setResetLoading(false); return; } @@ -84,10 +86,10 @@ export function PasswordReset({userInfo}: PasswordResetProps) { try { await completePasswordReset(userInfo.username, tempToken, newPassword); - toast.success("Password reset successfully! You can now log in with your new password."); + toast.success(t('common.passwordResetSuccess')); resetPasswordState(); } catch (err: any) { - setError(err?.response?.data?.error || "Failed to complete password reset"); + setError(err?.response?.data?.error || t('common.failedToCompletePasswordReset')); } finally { setResetLoading(false); } @@ -105,10 +107,10 @@ export function PasswordReset({userInfo}: PasswordResetProps) { - Password + {t('common.password')} - Change your account password + {t('common.changeAccountPassword')} @@ -122,7 +124,7 @@ export function PasswordReset({userInfo}: PasswordResetProps) { disabled={resetLoading || !userInfo.username.trim()} onClick={handleInitiatePasswordReset} > - {resetLoading ? Spinner : "Send Reset Code"} + {resetLoading ? Spinner : t('common.sendResetCode')}
@@ -131,12 +133,11 @@ export function PasswordReset({userInfo}: PasswordResetProps) { {resetStep === "verify" && ( <>
-

Enter the 6-digit code from the docker container logs for - user: {userInfo.username}

+

{t('common.enterSixDigitCode')} {userInfo.username}

- + setResetCode(e.target.value.replace(/\D/g, ''))} disabled={resetLoading} - placeholder="000000" + placeholder={t('placeholders.enterCode')} />
@@ -176,12 +177,11 @@ export function PasswordReset({userInfo}: PasswordResetProps) { {resetStep === "newPassword" && ( <>
-

Enter your new password for - user: {userInfo.username}

+

{t('common.enterNewPassword')} {userInfo.username}

- +
- + - {resetLoading ? Spinner : "Reset Password"} + {resetLoading ? Spinner : t('common.resetPassword')}
)} {error && ( - Error + {t('common.error')} {error} )}