Fix env not loading after restart, update translsations, fix export DB nginx conf
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 隧道",
|
||||
|
||||
@@ -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"}`,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -845,7 +845,6 @@ export function HomepageAuth({
|
||||
|
||||
{resetStep === "verify" && (
|
||||
<>
|
||||
o
|
||||
<div className="text-center text-muted-foreground mb-4">
|
||||
<p>
|
||||
{t("auth.enterResetCode")}{" "}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user