diff --git a/docker/nginx-https.conf b/docker/nginx-https.conf index fe780ebe..4e92a0e2 100644 --- a/docker/nginx-https.conf +++ b/docker/nginx-https.conf @@ -91,12 +91,41 @@ http { } location ~ ^/database(/.*)?$ { + client_max_body_size 5G; + client_body_timeout 300s; + 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; + + proxy_connect_timeout 60s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + + proxy_request_buffering off; + proxy_buffering off; + } + + location ~ ^/db(/.*)?$ { + client_max_body_size 5G; + client_body_timeout 300s; + + 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; + + proxy_connect_timeout 60s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + + proxy_request_buffering off; + proxy_buffering off; } location ~ ^/encryption(/.*)?$ { diff --git a/docker/nginx.conf b/docker/nginx.conf index 19583dee..3c544d3d 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -79,12 +79,41 @@ http { } location ~ ^/database(/.*)?$ { + client_max_body_size 5G; + client_body_timeout 300s; + 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; + + proxy_connect_timeout 60s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + + proxy_request_buffering off; + proxy_buffering off; + } + + location ~ ^/db(/.*)?$ { + client_max_body_size 5G; + client_body_timeout 300s; + + 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; + + proxy_connect_timeout 60s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + + proxy_request_buffering off; + proxy_buffering off; } location ~ ^/encryption(/.*)?$ { diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index b96089f9..fc63a1cb 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -207,7 +207,9 @@ async function fetchGitHubAPI( } } -app.use(bodyParser.json()); +app.use(bodyParser.json({ limit: "1gb" })); +app.use(bodyParser.urlencoded({ limit: "1gb", extended: true })); +app.use(bodyParser.raw({ limit: "5gb", type: "application/octet-stream" })); app.use(cookieParser()); app.get("/health", (req, res) => { @@ -494,14 +496,29 @@ app.post("/database/export", authenticateJWT, async (req, res) => { 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 }); + + try { + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } + } catch (dirError) { + apiLogger.error("Failed to create temp directory", dirError, { + operation: "export_temp_dir_error", + tempDir, + }); + throw new Error(`Failed to create temp directory: ${dirError.message}`); } const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); const filename = `termix-export-${user[0].username}-${timestamp}.sqlite`; const tempPath = path.join(tempDir, filename); + apiLogger.info("Creating export database", { + operation: "export_db_creation", + userId, + tempPath, + }); + const exportDb = new Database(tempPath); try { @@ -844,21 +861,40 @@ app.post("/database/export", authenticateJWT, async (req, res) => { res.setHeader("Content-Disposition", `attachment; filename="${filename}"`); const fileStream = fs.createReadStream(tempPath); - fileStream.pipe(res); + + fileStream.on("error", (streamError) => { + apiLogger.error("File stream error during export", streamError, { + operation: "export_file_stream_error", + userId, + tempPath, + }); + if (!res.headersSent) { + res.status(500).json({ + error: "Failed to stream export file", + details: streamError.message, + }); + } + }); fileStream.on("end", () => { + apiLogger.success("User data exported as SQLite successfully", { + operation: "user_data_sqlite_export_success", + userId, + filename, + }); + fs.unlink(tempPath, (err) => { if (err) { - apiLogger.warn("Failed to clean up export file", { path: tempPath }); + apiLogger.warn("Failed to clean up export file", { + operation: "export_cleanup_failed", + path: tempPath, + error: err.message + }); } }); }); - apiLogger.success("User data exported as SQLite successfully", { - operation: "user_data_sqlite_export_success", - userId, - filename, - }); + fileStream.pipe(res); } catch (error) { apiLogger.error("User data SQLite export failed", error, { operation: "user_data_sqlite_export_failed", diff --git a/src/backend/utils/database-migration.ts b/src/backend/utils/database-migration.ts index 06dc2f5e..f42d57ea 100644 --- a/src/backend/utils/database-migration.ts +++ b/src/backend/utils/database-migration.ts @@ -54,9 +54,29 @@ export class DatabaseMigration { let reason = ""; if (hasEncryptedDb && hasUnencryptedDb) { - needsMigration = false; - reason = - "Both encrypted and unencrypted databases exist. Skipping migration for safety. Manual intervention may be required."; + const unencryptedSize = fs.statSync(this.unencryptedDbPath).size; + const encryptedSize = fs.statSync(this.encryptedDbPath).size; + + if (unencryptedSize === 0) { + needsMigration = false; + reason = "Empty unencrypted database found alongside encrypted database. Removing empty file."; + try { + fs.unlinkSync(this.unencryptedDbPath); + databaseLogger.info("Removed empty unencrypted database file", { + operation: "migration_cleanup_empty", + path: this.unencryptedDbPath, + }); + } catch (error) { + databaseLogger.warn("Failed to remove empty unencrypted database", { + operation: "migration_cleanup_empty_failed", + error: error instanceof Error ? error.message : "Unknown error", + }); + } + } else { + needsMigration = false; + reason = + "Both encrypted and unencrypted databases exist. Skipping migration for safety. Manual intervention may be required."; + } } else if (hasEncryptedDb && !hasUnencryptedDb) { needsMigration = false; reason = "Only encrypted database exists. No migration needed."; diff --git a/src/backend/utils/system-crypto.ts b/src/backend/utils/system-crypto.ts index 8910cbda..2e7c14e1 100644 --- a/src/backend/utils/system-crypto.ts +++ b/src/backend/utils/system-crypto.ts @@ -26,6 +26,20 @@ class SystemCrypto { return; } + const dataDir = process.env.DATA_DIR || "./db/data"; + const envPath = path.join(dataDir, ".env"); + + try { + const envContent = await fs.readFile(envPath, "utf8"); + const jwtMatch = envContent.match(/^JWT_SECRET=(.+)$/m); + if (jwtMatch && jwtMatch[1] && jwtMatch[1].length >= 64) { + this.jwtSecret = jwtMatch[1]; + process.env.JWT_SECRET = jwtMatch[1]; + return; + } + } catch { + } + await this.generateAndGuideUser(); } catch (error) { databaseLogger.error("Failed to initialize JWT secret", error, { @@ -50,6 +64,20 @@ class SystemCrypto { return; } + const dataDir = process.env.DATA_DIR || "./db/data"; + const envPath = path.join(dataDir, ".env"); + + try { + const envContent = await fs.readFile(envPath, "utf8"); + const dbKeyMatch = envContent.match(/^DATABASE_KEY=(.+)$/m); + if (dbKeyMatch && dbKeyMatch[1] && dbKeyMatch[1].length >= 64) { + this.databaseKey = Buffer.from(dbKeyMatch[1], "hex"); + process.env.DATABASE_KEY = dbKeyMatch[1]; + return; + } + } catch { + } + await this.generateAndGuideDatabaseKey(); } catch (error) { databaseLogger.error("Failed to initialize database key", error, { @@ -74,6 +102,20 @@ class SystemCrypto { return; } + const dataDir = process.env.DATA_DIR || "./db/data"; + const envPath = path.join(dataDir, ".env"); + + try { + const envContent = await fs.readFile(envPath, "utf8"); + const tokenMatch = envContent.match(/^INTERNAL_AUTH_TOKEN=(.+)$/m); + if (tokenMatch && tokenMatch[1] && tokenMatch[1].length >= 32) { + this.internalAuthToken = tokenMatch[1]; + process.env.INTERNAL_AUTH_TOKEN = tokenMatch[1]; + return; + } + } catch { + } + await this.generateAndGuideInternalAuthToken(); } catch (error) { databaseLogger.error("Failed to initialize internal auth token", error, { diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index ea0ed0f1..ecf72a6b 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -267,6 +267,7 @@ "newVersionAvailable": "A new version ({{version}}) is available.", "failedToFetchUpdateInfo": "Failed to fetch update information", "preRelease": "Pre-release", + "loginFailed": "Login failed", "noReleasesFound": "No releases found.", "yourBackupCodes": "Your Backup Codes", "sendResetCode": "Send Reset Code", @@ -1406,6 +1407,10 @@ }, "mobile": { "selectHostToStart": "Select a host to start your terminal session", - "limitedSupportMessage": "Mobile support is currently limited. A dedicated mobile app is coming soon to enhance your experience." + "limitedSupportMessage": "Website mobile support is still in progress. Use the mobile app for a better experience.", + "mobileAppInProgress": "Mobile app is in progress", + "mobileAppInProgressDesc": "We're working on a dedicated mobile app to provide a better experience on mobile devices.", + "viewMobileAppDocs": "Install Mobile App", + "mobileAppDocumentation": "Mobile App Documentation" } } diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index 3c03045e..91e2aa0c 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -264,6 +264,7 @@ "newVersionAvailable": "有新版本 ({{version}}) 可用。", "failedToFetchUpdateInfo": "获取更新信息失败", "preRelease": "预发布版本", + "loginFailed": "登录失败", "noReleasesFound": "未找到发布版本。", "yourBackupCodes": "您的备份代码", "sendResetCode": "发送重置代码", @@ -1313,6 +1314,10 @@ }, "mobile": { "selectHostToStart": "选择一个主机以开始您的终端会话", - "limitedSupportMessage": "移动端支持目前有限。我们即将推出专门的移动应用以提升您的体验。" + "limitedSupportMessage": "网站移动端支持仍在开发中。使用移动应用以获得更好的体验。", + "mobileAppInProgress": "移动应用开发中", + "mobileAppInProgressDesc": "我们正在开发专门的移动应用,为移动设备提供更好的体验。", + "viewMobileAppDocs": "安装移动应用", + "mobileAppDocumentation": "移动应用文档" } } diff --git a/src/ui/Desktop/Admin/AdminSettings.tsx b/src/ui/Desktop/Admin/AdminSettings.tsx index 1fad2851..0ec10d28 100644 --- a/src/ui/Desktop/Admin/AdminSettings.tsx +++ b/src/ui/Desktop/Admin/AdminSettings.tsx @@ -273,7 +273,7 @@ export function AdminSettings({ try { const apiUrl = isElectron() ? `${(window as any).configuredServerUrl}/database/export` - : "/database/export"; + : `http://localhost:30001/database/export`; const response = await fetch(apiUrl, { method: "POST", @@ -333,7 +333,7 @@ export function AdminSettings({ try { const apiUrl = isElectron() ? `${(window as any).configuredServerUrl}/database/import` - : "/database/import"; + : `http://localhost:30001/database/import`; const formData = new FormData(); formData.append("file", importFile); diff --git a/src/ui/Desktop/Apps/Terminal/Terminal.tsx b/src/ui/Desktop/Apps/Terminal/Terminal.tsx index f66a4d49..a843be16 100644 --- a/src/ui/Desktop/Apps/Terminal/Terminal.tsx +++ b/src/ui/Desktop/Apps/Terminal/Terminal.tsx @@ -184,7 +184,8 @@ export const Terminal = forwardRef(function SSHTerminal( isUnmountingRef.current || shouldNotReconnectRef.current || isReconnectingRef.current || - isConnectingRef.current + isConnectingRef.current || + wasDisconnectedBySSH.current ) { return; } @@ -213,7 +214,7 @@ export const Terminal = forwardRef(function SSHTerminal( ); reconnectTimeoutRef.current = setTimeout(() => { - if (isUnmountingRef.current || shouldNotReconnectRef.current) { + if (isUnmountingRef.current || shouldNotReconnectRef.current || wasDisconnectedBySSH.current) { isReconnectingRef.current = false; return; } @@ -384,6 +385,7 @@ export const Terminal = forwardRef(function SSHTerminal( terminal.clear(); } setIsConnecting(true); + wasDisconnectedBySSH.current = false; attemptReconnection(); return; } @@ -408,9 +410,9 @@ export const Terminal = forwardRef(function SSHTerminal( if (terminal) { terminal.clear(); } - setIsConnecting(true); - if (!isUnmountingRef.current && !shouldNotReconnectRef.current) { - attemptReconnection(); + setIsConnecting(false); + if (onClose) { + onClose(); } } } catch (error) { @@ -438,12 +440,13 @@ export const Terminal = forwardRef(function SSHTerminal( return; } - setIsConnecting(true); + setIsConnecting(false); if ( !wasDisconnectedBySSH.current && !isUnmountingRef.current && !shouldNotReconnectRef.current ) { + wasDisconnectedBySSH.current = false; attemptReconnection(); } }); @@ -455,8 +458,9 @@ export const Terminal = forwardRef(function SSHTerminal( if (terminal) { terminal.clear(); } - setIsConnecting(true); + setIsConnecting(false); if (!isUnmountingRef.current && !shouldNotReconnectRef.current) { + wasDisconnectedBySSH.current = false; attemptReconnection(); } }); diff --git a/src/ui/Mobile/Homepage/HomepageAuth.tsx b/src/ui/Mobile/Homepage/HomepageAuth.tsx index fb790b8c..0329a2e2 100644 --- a/src/ui/Mobile/Homepage/HomepageAuth.tsx +++ b/src/ui/Mobile/Homepage/HomepageAuth.tsx @@ -534,6 +534,28 @@ export function HomepageAuth({ )} + {internalLoggedIn && !authLoading && ( +
+
+

+ {t("homepage.loggedInTitle")} +

+

+ {t("mobile.mobileAppInProgressDesc")} +

+
+ + +
+ )} + {!internalLoggedIn && !authLoading && !totpRequired && ( <>
@@ -855,6 +877,17 @@ export function HomepageAuth({
+ +
+ +
)} diff --git a/src/ui/Mobile/MobileApp.tsx b/src/ui/Mobile/MobileApp.tsx index e8367ae6..8d8f014e 100644 --- a/src/ui/Mobile/MobileApp.tsx +++ b/src/ui/Mobile/MobileApp.tsx @@ -162,6 +162,12 @@ const AppContent: FC = () => {

{t("mobile.limitedSupportMessage")}

+ )}