diff --git a/docker/nginx-https.conf b/docker/nginx-https.conf index a6007a7b..44aa38fb 100644 --- a/docker/nginx-https.conf +++ b/docker/nginx-https.conf @@ -90,6 +90,24 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } + location ~ ^/database(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location ~ ^/encryption(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + location /ssh/ { proxy_pass http://127.0.0.1:30001; proxy_http_version 1.1; diff --git a/docker/nginx.conf b/docker/nginx.conf index e422c585..349e988f 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -76,6 +76,24 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } + location ~ ^/database(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location ~ ^/encryption(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + location /ssh/ { proxy_pass http://127.0.0.1:30001; proxy_http_version 1.1; diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index 10499049..8ed95be0 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -17,8 +17,6 @@ import { DataCrypto } from "../utils/data-crypto.js"; import { DatabaseFileEncryption } from "../utils/database-file-encryption.js"; import { DatabaseMigration } from "../utils/database-migration.js"; import { UserDataExport } from "../utils/user-data-export.js"; -import { UserDataImport } from "../utils/user-data-import.js"; -import https from "https"; import { AutoSSLSetup } from "../utils/auto-ssl-setup.js"; import { eq, and } from "drizzle-orm"; import { users, sshData, sshCredentials, fileManagerRecent, fileManagerPinned, fileManagerShortcuts, dismissedAlerts, sshCredentialUsage, settings } from "./db/schema.js"; @@ -447,7 +445,10 @@ app.post("/database/export", authenticateJWT, async (req, res) => { } // Create temporary SQLite database - const tempDir = path.join(os.tmpdir(), 'termix-exports'); + // Use app data directory for temp files in Docker environments + const tempDir = process.env.NODE_ENV === 'production' + ? path.join(process.env.DATA_DIR || './db/data', '.temp', 'exports') + : path.join(os.tmpdir(), 'termix-exports'); if (!fs.existsSync(tempDir)) { fs.mkdirSync(tempDir, { recursive: true }); } diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 4795e261..83a8ab8a 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -931,7 +931,17 @@ "operationCompletedSuccessfully": "{{operation}} {{count}} items successfully", "operationCompleted": "{{operation}} {{count}} items", "downloadFileSuccess": "File {{name}} downloaded successfully", - "downloadFileFailed": "Download failed" + "downloadFileFailed": "Download failed", + "moveTo": "Move to {{name}}", + "diffCompareWith": "Diff compare with {{name}}", + "dragOutsideToDownload": "Drag outside window to download ({{count}} files)", + "newFolderDefault": "NewFolder", + "newFileDefault": "NewFile.txt", + "successfullyMovedItems": "Successfully moved {{count}} items to {{target}}", + "move": "Move", + "searchInFile": "Search in file (Ctrl+F)", + "showKeyboardShortcuts": "Show keyboard shortcuts", + "startWritingMarkdown": "Start writing your markdown content..." }, "tunnels": { "title": "SSH Tunnels", diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index 743cd36b..3e439676 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -945,7 +945,17 @@ "operationCompletedSuccessfully": "已{{operation}} {{count}} 个项目", "operationCompleted": "已{{operation}} {{count}} 个项目", "downloadFileSuccess": "文件 {{name}} 下载成功", - "downloadFileFailed": "下载失败" + "downloadFileFailed": "下载失败", + "moveTo": "移动到 {{name}}", + "diffCompareWith": "与 {{name}} 对比", + "dragOutsideToDownload": "拖拽到窗口外下载 ({{count}} 个文件)", + "newFolderDefault": "新文件夹", + "newFileDefault": "新文件.txt", + "successfullyMovedItems": "成功移动 {{count}} 个项目到 {{target}}", + "move": "移动", + "searchInFile": "在文件中搜索 (Ctrl+F)", + "showKeyboardShortcuts": "显示键盘快捷键", + "startWritingMarkdown": "开始编写您的 markdown 内容..." }, "tunnels": { "title": "SSH 隧道", diff --git a/src/ui/Desktop/Admin/AdminSettings.tsx b/src/ui/Desktop/Admin/AdminSettings.tsx index fbe8d28b..d4d9645b 100644 --- a/src/ui/Desktop/Admin/AdminSettings.tsx +++ b/src/ui/Desktop/Admin/AdminSettings.tsx @@ -389,6 +389,11 @@ export function AdminSettings({ ); setImportFile(null); setImportPassword(""); + + // Refresh the page to show imported data + setTimeout(() => { + window.location.reload(); + }, 1500); } else { toast.error( `${t("admin.databaseImportFailed")}: ${result.summary?.errors?.join(", ") || "Unknown error"}`, diff --git a/src/ui/Desktop/Apps/File Manager/FileManager.tsx b/src/ui/Desktop/Apps/File Manager/FileManager.tsx index f10f8920..bc17f069 100644 --- a/src/ui/Desktop/Apps/File Manager/FileManager.tsx +++ b/src/ui/Desktop/Apps/File Manager/FileManager.tsx @@ -657,7 +657,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { // Linus-style creation: pure intent, no side effects function handleCreateNewFolder() { - const defaultName = generateUniqueName("NewFolder", "directory"); + const defaultName = generateUniqueName(t("fileManager.newFolderDefault"), "directory"); const newCreateIntent = { id: Date.now().toString(), type: 'directory' as const, @@ -670,7 +670,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { } function handleCreateNewFile() { - const defaultName = generateUniqueName("NewFile.txt", "file"); + const defaultName = generateUniqueName(t("fileManager.newFileDefault"), "file"); const newCreateIntent = { id: Date.now().toString(), type: 'file' as const, diff --git a/src/ui/Desktop/Apps/File Manager/FileManagerGrid.tsx b/src/ui/Desktop/Apps/File Manager/FileManagerGrid.tsx index 9576101e..545ec0a4 100644 --- a/src/ui/Desktop/Apps/File Manager/FileManagerGrid.tsx +++ b/src/ui/Desktop/Apps/File Manager/FileManagerGrid.tsx @@ -1026,13 +1026,13 @@ export function FileManagerGrid({ /> @@ -1061,7 +1061,7 @@ export function FileManagerGrid({ ))} @@ -484,7 +484,7 @@ export function FileViewer({ size="sm" onClick={() => setShowKeyboardShortcuts(!showKeyboardShortcuts)} className="flex items-center gap-2" - title="Show keyboard shortcuts" + title={t("fileManager.showKeyboardShortcuts")} > @@ -928,7 +928,7 @@ export function FileViewer({ onContentChange?.(e.target.value); }} className="w-full h-full resize-none border-0 bg-transparent text-foreground font-mono text-sm leading-relaxed focus:outline-none focus:ring-0" - placeholder="Start writing your markdown content..." + placeholder={t("fileManager.startWritingMarkdown")} /> diff --git a/src/ui/Desktop/Homepage/HomepageAuth.tsx b/src/ui/Desktop/Homepage/HomepageAuth.tsx index 93628fee..3f8189b6 100644 --- a/src/ui/Desktop/Homepage/HomepageAuth.tsx +++ b/src/ui/Desktop/Homepage/HomepageAuth.tsx @@ -845,7 +845,6 @@ export function HomepageAuth({ {resetStep === "verify" && ( <> - o

{t("auth.enterResetCode")}{" "} diff --git a/src/ui/Mobile/Homepage/HomepageAuth.tsx b/src/ui/Mobile/Homepage/HomepageAuth.tsx index ae91956c..98081237 100644 --- a/src/ui/Mobile/Homepage/HomepageAuth.tsx +++ b/src/ui/Mobile/Homepage/HomepageAuth.tsx @@ -6,6 +6,7 @@ import { Label } from "@/components/ui/label.tsx"; import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert.tsx"; import { useTranslation } from "react-i18next"; import { LanguageSwitcher } from "@/ui/Desktop/User/LanguageSwitcher.tsx"; +import { toast } from "sonner"; import { registerUser, loginUser, @@ -92,6 +93,12 @@ export function HomepageAuth({ }); }, []); + useEffect(() => { + if (!registrationAllowed && !internalLoggedIn) { + toast.warning(t("messages.registrationDisabled")); + } + }, [registrationAllowed, internalLoggedIn, t]); + useEffect(() => { getOIDCConfig() .then((response) => { @@ -116,6 +123,7 @@ export function HomepageAuth({ if (res.setup_required) { setFirstUser(true); setTab("signup"); + toast.info(t("auth.firstUserMessage")); } else { setFirstUser(false); } @@ -124,7 +132,7 @@ export function HomepageAuth({ .catch(() => { setDbError(t("errors.databaseConnection")); }); - }, [setDbError]); + }, [setDbError, t]); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); @@ -132,7 +140,7 @@ export function HomepageAuth({ setLoading(true); if (!localUsername.trim()) { - setError(t("errors.requiredField")); + toast.error(t("errors.requiredField")); setLoading(false); return; } @@ -143,12 +151,12 @@ export function HomepageAuth({ res = await loginUser(localUsername, password); } else { if (password !== signupConfirmPassword) { - setError(t("errors.passwordMismatch")); + toast.error(t("errors.passwordMismatch")); setLoading(false); return; } if (password.length < 6) { - setError(t("errors.minLength", { min: 6 })); + toast.error(t("errors.minLength", { min: 6 })); setLoading(false); return; } @@ -185,12 +193,15 @@ export function HomepageAuth({ setInternalLoggedIn(true); if (tab === "signup") { setSignupConfirmPassword(""); + toast.success(t("messages.registrationSuccess")); + } else { + toast.success(t("messages.loginSuccess")); } setTotpRequired(false); setTotpCode(""); setTotpTempToken(""); } catch (err: any) { - setError( + toast.error( err?.response?.data?.error || err?.message || t("errors.unknownError"), ); setInternalLoggedIn(false); @@ -215,9 +226,9 @@ export function HomepageAuth({ try { const result = await initiatePasswordReset(localUsername); setResetStep("verify"); - setError(null); + toast.success(t("messages.resetCodeSent")); } catch (err: any) { - setError( + toast.error( err?.response?.data?.error || err?.message || t("errors.failedPasswordReset"), @@ -234,9 +245,9 @@ export function HomepageAuth({ const response = await verifyPasswordResetCode(localUsername, resetCode); setTempToken(response.tempToken); setResetStep("newPassword"); - setError(null); + toast.success(t("messages.codeVerified")); } catch (err: any) { - setError(err?.response?.data?.error || t("errors.failedVerifyCode")); + toast.error(err?.response?.data?.error || t("errors.failedVerifyCode")); } finally { setResetLoading(false); } @@ -247,13 +258,13 @@ export function HomepageAuth({ setResetLoading(true); if (newPassword !== confirmPassword) { - setError(t("errors.passwordMismatch")); + toast.error(t("errors.passwordMismatch")); setResetLoading(false); return; } if (newPassword.length < 6) { - setError(t("errors.minLength", { min: 6 })); + toast.error(t("errors.minLength", { min: 6 })); setResetLoading(false); return; } @@ -269,8 +280,9 @@ export function HomepageAuth({ setError(null); setResetSuccess(true); + toast.success(t("messages.passwordResetSuccess")); } catch (err: any) { - setError(err?.response?.data?.error || t("errors.failedCompleteReset")); + toast.error(err?.response?.data?.error || t("errors.failedCompleteReset")); } finally { setResetLoading(false); } @@ -295,7 +307,7 @@ export function HomepageAuth({ async function handleTOTPVerification() { if (totpCode.length !== 6) { - setError(t("auth.enterCode")); + toast.error(t("auth.enterCode")); return; } @@ -327,8 +339,9 @@ export function HomepageAuth({ setTotpRequired(false); setTotpCode(""); setTotpTempToken(""); + toast.success(t("messages.loginSuccess")); } catch (err: any) { - setError( + toast.error( err?.response?.data?.error || err?.message || t("errors.invalidTotpCode"), @@ -351,7 +364,7 @@ export function HomepageAuth({ window.location.replace(authUrl); } catch (err: any) { - setError( + toast.error( err?.response?.data?.error || err?.message || t("errors.failedOidcLogin"), @@ -367,7 +380,7 @@ export function HomepageAuth({ const error = urlParams.get("error"); if (error) { - setError(`${t("errors.oidcAuthFailed")}: ${error}`); + toast.error(`${t("errors.oidcAuthFailed")}: ${error}`); setOidcLoading(false); window.history.replaceState({}, document.title, window.location.pathname); return; @@ -399,7 +412,7 @@ export function HomepageAuth({ ); }) .catch((err) => { - setError(t("errors.failedUserInfo")); + toast.error(t("errors.failedUserInfo")); setInternalLoggedIn(false); setLoggedIn(false); setIsAdmin(false); @@ -451,31 +464,6 @@ export function HomepageAuth({ {dbError} )} - {firstUser && !dbError && !internalLoggedIn && ( - - {t("auth.firstUser")} - - {t("auth.firstUserMessage")}{" "} - - GitHub Issue - - . - - - )} - {!registrationAllowed && !internalLoggedIn && ( - - {t("auth.registerTitle")} - - {t("messages.registrationDisabled")} - - - )} {totpRequired && (

@@ -709,14 +697,11 @@ export function HomepageAuth({ {resetSuccess && ( <> - - - {t("auth.passwordResetSuccess")} - - +
+

{t("auth.passwordResetSuccessDesc")} - - +

+
); } diff --git a/src/ui/Mobile/MobileApp.tsx b/src/ui/Mobile/MobileApp.tsx index aadba28a..689873c7 100644 --- a/src/ui/Mobile/MobileApp.tsx +++ b/src/ui/Mobile/MobileApp.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, type FC } from "react"; import { Terminal } from "@/ui/Mobile/Apps/Terminal/Terminal.tsx"; import { TerminalKeyboard } from "@/ui/Mobile/Apps/Terminal/TerminalKeyboard.tsx"; import { BottomNavbar } from "@/ui/Mobile/Navigation/BottomNavbar.tsx"; @@ -10,6 +10,7 @@ import { import { getUserInfo, getCookie } from "@/ui/main-axios.ts"; import { HomepageAuth } from "@/ui/Mobile/Homepage/HomepageAuth.tsx"; import { useTranslation } from "react-i18next"; +import { Toaster } from "@/components/ui/sonner.tsx"; const AppContent: FC = () => { const { t } = useTranslation(); @@ -159,7 +160,6 @@ const AppContent: FC = () => { ref={tab.terminalRef} hostConfig={tab.hostConfig} isVisible={tab.id === currentTab} - onClose={() => removeTab(tab.id)} />
))} @@ -207,6 +207,13 @@ const AppContent: FC = () => { />
+ ); };