Fix env not loading after restart, update translsations, fix export DB nginx conf

This commit is contained in:
LukeGus
2025-09-26 09:13:41 -05:00
parent 62bc684fff
commit 9b12515676
12 changed files with 125 additions and 78 deletions

View File

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

View File

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

View File

@@ -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 隧道",

View File

@@ -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"}`,

View File

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

View File

@@ -1026,13 +1026,13 @@ export function FileManagerGrid({
/>
<button
onClick={confirmEditingPath}
className="px-2 py-1 bg-primary text-primary-foreground rounded text-xs hover:bg-primary/80"
className="px-2 py-1 bg-primary text-primary-foreground rounded text-sm hover:bg-primary/80"
>
{t("fileManager.confirm")}
</button>
<button
onClick={cancelEditingPath}
className="px-2 py-1 bg-secondary text-secondary-foreground rounded text-xs hover:bg-secondary/80"
className="px-2 py-1 bg-secondary text-secondary-foreground rounded text-sm hover:bg-secondary/80"
>
{t("fileManager.cancel")}
</button>
@@ -1061,7 +1061,7 @@ export function FileManagerGrid({
))}
<button
onClick={startEditingPath}
className="ml-2 p-1 rounded hover:bg-dark-hover opacity-60 hover:opacity-100"
className="ml-2 p-1 rounded hover:bg-dark-hover opacity-60 hover:opacity-100 flex items-center justify-center"
title={t("fileManager.editPath")}
>
<Edit className="w-3 h-3" />
@@ -1147,7 +1147,7 @@ export function FileManagerGrid({
data-file-path={file.path}
draggable={true}
className={cn(
"group p-3 rounded-lg cursor-pointer transition-all",
"group p-3 rounded-lg cursor-pointer",
"hover:bg-accent hover:text-accent-foreground border-2 border-transparent",
isSelected && "bg-primary/20 border-primary",
dragState.target?.path === file.path &&
@@ -1240,7 +1240,7 @@ export function FileManagerGrid({
data-file-path={file.path}
draggable={true}
className={cn(
"flex items-center gap-3 p-2 rounded cursor-pointer transition-all",
"flex items-center gap-3 p-2 rounded cursor-pointer",
"hover:bg-accent hover:text-accent-foreground",
isSelected && "bg-primary/20",
dragState.target?.path === file.path &&
@@ -1378,14 +1378,14 @@ export function FileManagerGrid({
<>
<Move className="w-4 h-4 text-blue-500" />
<span className="text-sm font-medium text-foreground">
Move to {dragState.target.name}
{t("fileManager.moveTo", { name: dragState.target.name })}
</span>
</>
) : (
<>
<GitCompare className="w-4 h-4 text-purple-500" />
<span className="text-sm font-medium text-foreground">
Diff compare with {dragState.target.name}
{t("fileManager.diffCompareWith", { name: dragState.target.name })}
</span>
</>
)
@@ -1393,7 +1393,7 @@ export function FileManagerGrid({
<>
<Download className="w-4 h-4 text-green-500" />
<span className="text-sm font-medium text-foreground">
Drag outside window to download ({files.length} files)
{t("fileManager.dragOutsideToDownload", { count: files.length })}
</span>
</>
);
@@ -1436,7 +1436,7 @@ function CreateIntentGridItem({
};
return (
<div className="group p-3 rounded-lg border-2 border-dashed border-primary bg-primary/10 transition-all">
<div className="group p-3 rounded-lg border-2 border-dashed border-primary bg-primary/10">
<div className="flex flex-col items-center text-center">
<div className="mb-2">
{intent.type === 'directory' ? (
@@ -1490,7 +1490,7 @@ function CreateIntentListItem({
};
return (
<div className="flex items-center gap-3 p-2 rounded border-2 border-dashed border-primary bg-primary/10 transition-all">
<div className="flex items-center gap-3 p-2 rounded border-2 border-dashed border-primary bg-primary/10">
<div className="flex-shrink-0">
{intent.type === 'directory' ? (
<Folder className="w-6 h-6 text-primary" />

View File

@@ -472,7 +472,7 @@ export function FileViewer({
}
}}
className="flex items-center gap-2"
title="Search in file (Ctrl+F)"
title={t("fileManager.searchInFile")}
>
<Search className="w-4 h-4" />
</Button>
@@ -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")}
>
<Keyboard className="w-4 h-4" />
</Button>
@@ -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")}
/>
</div>
</div>

View File

@@ -845,7 +845,6 @@ export function HomepageAuth({
{resetStep === "verify" && (
<>
o
<div className="text-center text-muted-foreground mb-4">
<p>
{t("auth.enterResetCode")}{" "}

View File

@@ -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({
<AlertDescription>{dbError}</AlertDescription>
</Alert>
)}
{firstUser && !dbError && !internalLoggedIn && (
<Alert variant="default" className="mb-4">
<AlertTitle>{t("auth.firstUser")}</AlertTitle>
<AlertDescription className="inline">
{t("auth.firstUserMessage")}{" "}
<a
href="https://github.com/LukeGus/Termix/issues/new"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 underline hover:text-blue-800 inline"
>
GitHub Issue
</a>
.
</AlertDescription>
</Alert>
)}
{!registrationAllowed && !internalLoggedIn && (
<Alert variant="destructive" className="mb-4">
<AlertTitle>{t("auth.registerTitle")}</AlertTitle>
<AlertDescription>
{t("messages.registrationDisabled")}
</AlertDescription>
</Alert>
)}
{totpRequired && (
<div className="flex flex-col gap-5">
<div className="mb-6 text-center">
@@ -709,14 +697,11 @@ export function HomepageAuth({
{resetSuccess && (
<>
<Alert className="mb-4">
<AlertTitle>
{t("auth.passwordResetSuccess")}
</AlertTitle>
<AlertDescription>
<div className="text-center p-4 bg-green-500/10 rounded-lg border border-green-500/20 mb-4">
<p className="text-green-400 text-sm">
{t("auth.passwordResetSuccessDesc")}
</AlertDescription>
</Alert>
</p>
</div>
<Button
type="button"
className="w-full h-11 text-base font-semibold"
@@ -881,12 +866,6 @@ export function HomepageAuth({
</div>
</>
)}
{error && (
<Alert variant="destructive" className="mt-4">
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
</div>
);
}

View File

@@ -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)}
/>
</div>
))}
@@ -207,6 +207,13 @@ const AppContent: FC = () => {
/>
</div>
</div>
<Toaster
position="bottom-center"
richColors={false}
closeButton
duration={5000}
offset={20}
/>
</div>
);
};