diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index 36f6f070..eb768181 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -31,8 +31,13 @@ import { dismissedAlerts, sshCredentialUsage, settings, - snippets, } from "./db/schema.js"; +import type { + CacheEntry, + GitHubRelease, + GitHubAPIResponse, + AuthenticatedRequest, +} from "../../types/index.js"; import { getDb } from "./db/index.js"; import Database from "better-sqlite3"; @@ -107,17 +112,11 @@ const upload = multer({ }, }); -interface CacheEntry { - data: any; - timestamp: number; - expiresAt: number; -} - class GitHubCache { private cache: Map = new Map(); private readonly CACHE_DURATION = 30 * 60 * 1000; - set(key: string, data: any): void { + set(key: string, data: T): void { const now = Date.now(); this.cache.set(key, { data, @@ -126,7 +125,7 @@ class GitHubCache { }); } - get(key: string): any | null { + get(key: string): T | null { const entry = this.cache.get(key); if (!entry) { return null; @@ -137,7 +136,7 @@ class GitHubCache { return null; } - return entry.data; + return entry.data as T; } } @@ -147,34 +146,16 @@ const GITHUB_API_BASE = "https://api.github.com"; const REPO_OWNER = "LukeGus"; const REPO_NAME = "Termix"; -interface GitHubRelease { - id: number; - tag_name: string; - name: string; - body: string; - published_at: string; - html_url: string; - assets: Array<{ - id: number; - name: string; - size: number; - download_count: number; - browser_download_url: string; - }>; - prerelease: boolean; - draft: boolean; -} - -async function fetchGitHubAPI( +async function fetchGitHubAPI( endpoint: string, cacheKey: string, -): Promise { - const cachedData = githubCache.get(cacheKey); - if (cachedData) { +): Promise> { + const cachedEntry = githubCache.get>(cacheKey); + if (cachedEntry) { return { - data: cachedData, + data: cachedEntry.data, cached: true, - cache_age: Date.now() - cachedData.timestamp, + cache_age: Date.now() - cachedEntry.timestamp, }; } @@ -193,8 +174,13 @@ async function fetchGitHubAPI( ); } - const data = await response.json(); - githubCache.set(cacheKey, data); + const data = (await response.json()) as T; + const cacheData: CacheEntry = { + data, + timestamp: Date.now(), + expiresAt: Date.now() + 30 * 60 * 1000, + }; + githubCache.set(cacheKey, cacheData); return { data: data, @@ -274,7 +260,7 @@ app.get("/version", authenticateJWT, async (req, res) => { try { const cacheKey = "latest_release"; - const releaseData = await fetchGitHubAPI( + const releaseData = await fetchGitHubAPI( `/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`, cacheKey, ); @@ -325,12 +311,12 @@ app.get("/releases/rss", authenticateJWT, async (req, res) => { ); const cacheKey = `releases_rss_${page}_${per_page}`; - const releasesData = await fetchGitHubAPI( + const releasesData = await fetchGitHubAPI( `/repos/${REPO_OWNER}/${REPO_NAME}/releases?page=${page}&per_page=${per_page}`, cacheKey, ); - const rssItems = releasesData.data.map((release: GitHubRelease) => ({ + const rssItems = releasesData.data.map((release) => ({ id: release.id, title: release.name || release.tag_name, description: release.body, @@ -459,7 +445,7 @@ app.post("/encryption/regenerate-jwt", requireAdmin, async (req, res) => { app.post("/database/export", authenticateJWT, async (req, res) => { try { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { password } = req.body; if (!password) { @@ -913,7 +899,7 @@ app.post( return res.status(400).json({ error: "No file uploaded" }); } - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { password } = req.body; if (!password) { @@ -1321,7 +1307,7 @@ app.post( apiLogger.error("SQLite import failed", error, { operation: "sqlite_import_api_failed", - userId: (req as any).userId, + userId: (req as AuthenticatedRequest).userId, }); res.status(500).json({ error: "Failed to import SQLite data", @@ -1333,7 +1319,7 @@ app.post( app.post("/database/export/preview", authenticateJWT, async (req, res) => { try { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { scope = "user_data", includeCredentials = true } = req.body; const exportData = await UserDataExport.exportUserData(userId, { diff --git a/src/backend/database/routes/alerts.ts b/src/backend/database/routes/alerts.ts index f1f113ff..82a6489f 100644 --- a/src/backend/database/routes/alerts.ts +++ b/src/backend/database/routes/alerts.ts @@ -1,3 +1,8 @@ +import type { + AuthenticatedRequest, + CacheEntry, + TermixAlert, +} from "../../../types/index.js"; import express from "express"; import { db } from "../db/index.js"; import { dismissedAlerts } from "../db/schema.js"; @@ -6,17 +11,11 @@ import fetch from "node-fetch"; import { authLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; -interface CacheEntry { - data: any; - timestamp: number; - expiresAt: number; -} - class AlertCache { private cache: Map = new Map(); private readonly CACHE_DURATION = 5 * 60 * 1000; - set(key: string, data: any): void { + set(key: string, data: T): void { const now = Date.now(); this.cache.set(key, { data, @@ -25,7 +24,7 @@ class AlertCache { }); } - get(key: string): any | null { + get(key: string): T | null { const entry = this.cache.get(key); if (!entry) { return null; @@ -36,7 +35,7 @@ class AlertCache { return null; } - return entry.data; + return entry.data as T; } } @@ -47,20 +46,9 @@ const REPO_OWNER = "LukeGus"; const REPO_NAME = "Termix-Docs"; const ALERTS_FILE = "main/termix-alerts.json"; -interface TermixAlert { - id: string; - title: string; - message: string; - expiresAt: string; - priority?: "low" | "medium" | "high" | "critical"; - type?: "info" | "warning" | "error" | "success"; - actionUrl?: string; - actionText?: string; -} - async function fetchAlertsFromGitHub(): Promise { const cacheKey = "termix_alerts"; - const cachedData = alertCache.get(cacheKey); + const cachedData = alertCache.get(cacheKey); if (cachedData) { return cachedData; } @@ -115,7 +103,7 @@ const authenticateJWT = authManager.createAuthMiddleware(); // GET /alerts router.get("/", authenticateJWT, async (req, res) => { try { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const allAlerts = await fetchAlertsFromGitHub(); @@ -148,7 +136,7 @@ router.get("/", authenticateJWT, async (req, res) => { router.post("/dismiss", authenticateJWT, async (req, res) => { try { const { alertId } = req.body; - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; if (!alertId) { authLogger.warn("Missing alertId in dismiss request", { userId }); @@ -186,7 +174,7 @@ router.post("/dismiss", authenticateJWT, async (req, res) => { // GET /alerts/dismissed/:userId router.get("/dismissed", authenticateJWT, async (req, res) => { try { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const dismissedAlertRecords = await db .select({ @@ -211,7 +199,7 @@ router.get("/dismissed", authenticateJWT, async (req, res) => { router.delete("/dismiss", authenticateJWT, async (req, res) => { try { const { alertId } = req.body; - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; if (!alertId) { return res.status(400).json({ error: "Alert ID is required" }); diff --git a/src/backend/database/routes/credentials.ts b/src/backend/database/routes/credentials.ts index b85bb5ec..2f0c3ce4 100644 --- a/src/backend/database/routes/credentials.ts +++ b/src/backend/database/routes/credentials.ts @@ -1,3 +1,4 @@ +import type { AuthenticatedRequest } from "../../../types/index.js"; import express from "express"; import { db } from "../db/index.js"; import { sshCredentials, sshCredentialUsage, sshData } from "../db/schema.js"; @@ -27,7 +28,11 @@ function generateSSHKeyPair( } { try { let ssh2Type = keyType; - const options: any = {}; + const options: { + bits?: number; + passphrase?: string; + cipher?: string; + } = {}; if (keyType === "ssh-rsa") { ssh2Type = "rsa"; @@ -44,6 +49,7 @@ function generateSSHKeyPair( options.cipher = "aes128-cbc"; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any const keyPair = ssh2Utils.generateKeyPairSync(ssh2Type as any, options); return { @@ -62,7 +68,7 @@ function generateSSHKeyPair( const router = express.Router(); -function isNonEmptyString(val: any): val is string { +function isNonEmptyString(val: unknown): val is string { return typeof val === "string" && val.trim().length > 0; } @@ -77,7 +83,7 @@ router.post( authenticateJWT, requireDataAccess, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { name, description, @@ -224,7 +230,7 @@ router.get( authenticateJWT, requireDataAccess, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; if (!isNonEmptyString(userId)) { authLogger.warn("Invalid userId for credential fetch"); @@ -257,7 +263,7 @@ router.get( authenticateJWT, requireDataAccess, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; if (!isNonEmptyString(userId)) { authLogger.warn("Invalid userId for credential folder fetch"); @@ -295,7 +301,7 @@ router.get( authenticateJWT, requireDataAccess, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { id } = req.params; if (!isNonEmptyString(userId) || !id) { @@ -326,19 +332,19 @@ router.get( const output = formatCredentialOutput(credential); if (credential.password) { - (output as any).password = credential.password; + output.password = credential.password; } if (credential.key) { - (output as any).key = credential.key; + output.key = credential.key; } if (credential.private_key) { - (output as any).privateKey = credential.private_key; + output.privateKey = credential.private_key; } if (credential.public_key) { - (output as any).publicKey = credential.public_key; + output.publicKey = credential.public_key; } if (credential.key_password) { - (output as any).keyPassword = credential.key_password; + output.keyPassword = credential.key_password; } res.json(output); @@ -359,7 +365,7 @@ router.put( authenticateJWT, requireDataAccess, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { id } = req.params; const updateData = req.body; @@ -383,7 +389,7 @@ router.put( return res.status(404).json({ error: "Credential not found" }); } - const updateFields: any = {}; + const updateFields: Record = {}; if (updateData.name !== undefined) updateFields.name = updateData.name.trim(); @@ -495,7 +501,7 @@ router.delete( authenticateJWT, requireDataAccess, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { id } = req.params; if (!isNonEmptyString(userId) || !id) { @@ -594,7 +600,7 @@ router.post( "/:id/apply-to-host/:hostId", authenticateJWT, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { id: credentialId, hostId } = req.params; if (!isNonEmptyString(userId) || !credentialId || !hostId) { @@ -627,8 +633,8 @@ router.post( .update(sshData) .set({ credentialId: parseInt(credentialId), - username: credential.username, - authType: credential.auth_type || credential.authType, + username: credential.username as string, + authType: (credential.auth_type || credential.authType) as string, password: null, key: null, key_password: null, @@ -673,7 +679,7 @@ router.get( "/:id/hosts", authenticateJWT, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { id: credentialId } = req.params; if (!isNonEmptyString(userId) || !credentialId) { @@ -705,7 +711,9 @@ router.get( }, ); -function formatCredentialOutput(credential: any): any { +function formatCredentialOutput( + credential: Record, +): Record { return { id: credential.id, name: credential.name, @@ -729,7 +737,9 @@ function formatCredentialOutput(credential: any): any { }; } -function formatSSHHostOutput(host: any): any { +function formatSSHHostOutput( + host: Record, +): Record { return { id: host.id, userId: host.userId, @@ -749,7 +759,7 @@ function formatSSHHostOutput(host: any): any { enableTerminal: !!host.enableTerminal, enableTunnel: !!host.enableTunnel, tunnelConnections: host.tunnelConnections - ? JSON.parse(host.tunnelConnections) + ? JSON.parse(host.tunnelConnections as string) : [], enableFileManager: !!host.enableFileManager, defaultPath: host.defaultPath, @@ -764,7 +774,7 @@ router.put( "/folders/rename", authenticateJWT, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { oldName, newName } = req.body; if (!isNonEmptyString(oldName) || !isNonEmptyString(newName)) { @@ -1117,10 +1127,10 @@ router.post( ); async function deploySSHKeyToHost( - hostConfig: any, + hostConfig: Record, publicKey: string, // eslint-disable-next-line @typescript-eslint/no-unused-vars - _credentialData: any, + _credentialData: Record, ): Promise<{ success: boolean; message?: string; error?: string }> { return new Promise((resolve) => { const conn = new Client(); @@ -1364,7 +1374,7 @@ async function deploySSHKeyToHost( }); try { - const connectionConfig: any = { + const connectionConfig: Record = { host: hostConfig.ip, port: hostConfig.port || 22, username: hostConfig.username, @@ -1411,14 +1421,15 @@ async function deploySSHKeyToHost( connectionConfig.password = hostConfig.password; } else if (hostConfig.authType === "key" && hostConfig.privateKey) { try { + const privateKey = hostConfig.privateKey as string; if ( - !hostConfig.privateKey.includes("-----BEGIN") || - !hostConfig.privateKey.includes("-----END") + !privateKey.includes("-----BEGIN") || + !privateKey.includes("-----END") ) { throw new Error("Invalid private key format"); } - const cleanKey = hostConfig.privateKey + const cleanKey = privateKey .trim() .replace(/\r\n/g, "\n") .replace(/\r/g, "\n"); @@ -1473,7 +1484,7 @@ router.post( } try { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; if (!userId) { return res.status(401).json({ success: false, @@ -1540,7 +1551,7 @@ router.post( }; if (hostData.authType === "credential" && hostData.credentialId) { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; if (!userId) { return res.status(400).json({ success: false, @@ -1554,7 +1565,7 @@ router.post( db .select() .from(sshCredentials) - .where(eq(sshCredentials.id, hostData.credentialId)) + .where(eq(sshCredentials.id, hostData.credentialId as number)) .limit(1), "ssh_credentials", userId, @@ -1589,7 +1600,7 @@ router.post( const deployResult = await deploySSHKeyToHost( hostConfig, - credData.publicKey, + credData.publicKey as string, credData, ); diff --git a/src/backend/database/routes/snippets.ts b/src/backend/database/routes/snippets.ts index 23af7bf7..89dc4513 100644 --- a/src/backend/database/routes/snippets.ts +++ b/src/backend/database/routes/snippets.ts @@ -1,3 +1,4 @@ +import type { AuthenticatedRequest } from "../../../types/index.js"; import express from "express"; import { db } from "../db/index.js"; import { snippets } from "../db/schema.js"; @@ -8,7 +9,7 @@ import { AuthManager } from "../../utils/auth-manager.js"; const router = express.Router(); -function isNonEmptyString(val: any): val is string { +function isNonEmptyString(val: unknown): val is string { return typeof val === "string" && val.trim().length > 0; } @@ -23,7 +24,7 @@ router.get( authenticateJWT, requireDataAccess, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; if (!isNonEmptyString(userId)) { authLogger.warn("Invalid userId for snippets fetch"); @@ -52,12 +53,15 @@ router.get( authenticateJWT, requireDataAccess, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { id } = req.params; const snippetId = parseInt(id, 10); if (!isNonEmptyString(userId) || isNaN(snippetId)) { - authLogger.warn("Invalid request for snippet fetch: invalid ID", { userId, id }); + authLogger.warn("Invalid request for snippet fetch: invalid ID", { + userId, + id, + }); return res.status(400).json({ error: "Invalid request parameters" }); } @@ -88,7 +92,7 @@ router.post( authenticateJWT, requireDataAccess, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { name, content, description } = req.body; if ( @@ -139,7 +143,7 @@ router.put( authenticateJWT, requireDataAccess, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { id } = req.params; const updateData = req.body; @@ -158,7 +162,12 @@ router.put( return res.status(404).json({ error: "Snippet not found" }); } - const updateFields: any = { + const updateFields: Partial<{ + updatedAt: ReturnType; + name: string; + content: string; + description: string | null; + }> = { updatedAt: sql`CURRENT_TIMESTAMP`, }; @@ -206,7 +215,7 @@ router.delete( authenticateJWT, requireDataAccess, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { id } = req.params; if (!isNonEmptyString(userId) || !id) { diff --git a/src/backend/database/routes/ssh.ts b/src/backend/database/routes/ssh.ts index 870f423e..1102b66c 100644 --- a/src/backend/database/routes/ssh.ts +++ b/src/backend/database/routes/ssh.ts @@ -1,3 +1,4 @@ +import type { AuthenticatedRequest } from "../../../types/index.js"; import express from "express"; import { db } from "../db/index.js"; import { @@ -22,11 +23,11 @@ const router = express.Router(); const upload = multer({ storage: multer.memoryStorage() }); -function isNonEmptyString(value: any): value is string { +function isNonEmptyString(value: unknown): value is string { return typeof value === "string" && value.trim().length > 0; } -function isValidPort(port: any): port is number { +function isValidPort(port: unknown): port is number { return typeof port === "number" && port > 0 && port <= 65535; } @@ -74,7 +75,7 @@ router.get("/db/host/internal", async (req: Request, res: Response) => { : []; const hasAutoStartTunnels = tunnelConnections.some( - (tunnel: any) => tunnel.autoStart, + (tunnel: Record) => tunnel.autoStart, ); if (!hasAutoStartTunnels) { @@ -99,7 +100,7 @@ router.get("/db/host/internal", async (req: Request, res: Response) => { credentialId: host.credentialId, enableTunnel: true, tunnelConnections: tunnelConnections.filter( - (tunnel: any) => tunnel.autoStart, + (tunnel: Record) => tunnel.autoStart, ), pin: !!host.pin, enableTerminal: !!host.enableTerminal, @@ -183,8 +184,8 @@ router.post( requireDataAccess, upload.single("key"), async (req: Request, res: Response) => { - const userId = (req as any).userId; - let hostData: any; + const userId = (req as AuthenticatedRequest).userId; + let hostData: Record; if (req.headers["content-type"]?.includes("multipart/form-data")) { if (req.body.data) { @@ -251,7 +252,7 @@ router.post( } const effectiveAuthType = authType || authMethod; - const sshDataObj: any = { + const sshDataObj: Record = { userId: userId, name, folder: folder || null, @@ -321,11 +322,11 @@ router.post( enableTerminal: !!createdHost.enableTerminal, enableTunnel: !!createdHost.enableTunnel, tunnelConnections: createdHost.tunnelConnections - ? JSON.parse(createdHost.tunnelConnections) + ? JSON.parse(createdHost.tunnelConnections as string) : [], enableFileManager: !!createdHost.enableFileManager, statsConfig: createdHost.statsConfig - ? JSON.parse(createdHost.statsConfig) + ? JSON.parse(createdHost.statsConfig as string) : undefined, }; @@ -336,7 +337,7 @@ router.post( { operation: "host_create_success", userId, - hostId: createdHost.id, + hostId: createdHost.id as number, name, ip, port, @@ -367,8 +368,8 @@ router.put( upload.single("key"), async (req: Request, res: Response) => { const hostId = req.params.id; - const userId = (req as any).userId; - let hostData: any; + const userId = (req as AuthenticatedRequest).userId; + let hostData: Record; if (req.headers["content-type"]?.includes("multipart/form-data")) { if (req.body.data) { @@ -439,7 +440,7 @@ router.put( } const effectiveAuthType = authType || authMethod; - const sshDataObj: any = { + const sshDataObj: Record = { name, folder, tags: Array.isArray(tags) ? tags.join(",") : tags || "", @@ -526,11 +527,11 @@ router.put( enableTerminal: !!updatedHost.enableTerminal, enableTunnel: !!updatedHost.enableTunnel, tunnelConnections: updatedHost.tunnelConnections - ? JSON.parse(updatedHost.tunnelConnections) + ? JSON.parse(updatedHost.tunnelConnections as string) : [], enableFileManager: !!updatedHost.enableFileManager, statsConfig: updatedHost.statsConfig - ? JSON.parse(updatedHost.statsConfig) + ? JSON.parse(updatedHost.statsConfig as string) : undefined, }; @@ -568,7 +569,7 @@ router.put( // Route: Get SSH data for the authenticated user (requires JWT) // GET /ssh/host router.get("/db/host", authenticateJWT, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; if (!isNonEmptyString(userId)) { sshLogger.warn("Invalid userId for SSH data fetch", { operation: "host_fetch", @@ -584,7 +585,7 @@ router.get("/db/host", authenticateJWT, async (req: Request, res: Response) => { ); const result = await Promise.all( - data.map(async (row: any) => { + data.map(async (row: Record) => { const baseHost = { ...row, tags: @@ -597,11 +598,11 @@ router.get("/db/host", authenticateJWT, async (req: Request, res: Response) => { enableTerminal: !!row.enableTerminal, enableTunnel: !!row.enableTunnel, tunnelConnections: row.tunnelConnections - ? JSON.parse(row.tunnelConnections) + ? JSON.parse(row.tunnelConnections as string) : [], enableFileManager: !!row.enableFileManager, statsConfig: row.statsConfig - ? JSON.parse(row.statsConfig) + ? JSON.parse(row.statsConfig as string) : undefined, }; @@ -626,7 +627,7 @@ router.get( authenticateJWT, async (req: Request, res: Response) => { const hostId = req.params.id; - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; if (!isNonEmptyString(userId) || !hostId) { sshLogger.warn("Invalid userId or hostId for SSH host fetch by ID", { @@ -692,7 +693,7 @@ router.get( requireDataAccess, async (req: Request, res: Response) => { const hostId = req.params.id; - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; if (!isNonEmptyString(userId) || !hostId) { return res.status(400).json({ error: "Invalid userId or hostId" }); @@ -739,7 +740,7 @@ router.get( enableFileManager: !!resolvedHost.enableFileManager, defaultPath: resolvedHost.defaultPath, tunnelConnections: resolvedHost.tunnelConnections - ? JSON.parse(resolvedHost.tunnelConnections) + ? JSON.parse(resolvedHost.tunnelConnections as string) : [], }; @@ -767,7 +768,7 @@ router.delete( "/db/host/:id", authenticateJWT, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const hostId = req.params.id; if (!isNonEmptyString(userId) || !hostId) { @@ -866,7 +867,7 @@ router.get( "/file_manager/recent", authenticateJWT, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const hostId = req.query.hostId ? parseInt(req.query.hostId as string) : null; @@ -908,7 +909,7 @@ router.post( "/file_manager/recent", authenticateJWT, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { hostId, path, name } = req.body; if (!isNonEmptyString(userId) || !hostId || !path) { @@ -957,7 +958,7 @@ router.delete( "/file_manager/recent", authenticateJWT, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { hostId, path } = req.body; if (!isNonEmptyString(userId) || !hostId || !path) { @@ -990,7 +991,7 @@ router.get( "/file_manager/pinned", authenticateJWT, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const hostId = req.query.hostId ? parseInt(req.query.hostId as string) : null; @@ -1031,7 +1032,7 @@ router.post( "/file_manager/pinned", authenticateJWT, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { hostId, path, name } = req.body; if (!isNonEmptyString(userId) || !hostId || !path) { @@ -1077,7 +1078,7 @@ router.delete( "/file_manager/pinned", authenticateJWT, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { hostId, path } = req.body; if (!isNonEmptyString(userId) || !hostId || !path) { @@ -1110,7 +1111,7 @@ router.get( "/file_manager/shortcuts", authenticateJWT, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const hostId = req.query.hostId ? parseInt(req.query.hostId as string) : null; @@ -1151,7 +1152,7 @@ router.post( "/file_manager/shortcuts", authenticateJWT, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { hostId, path, name } = req.body; if (!isNonEmptyString(userId) || !hostId || !path) { @@ -1197,7 +1198,7 @@ router.delete( "/file_manager/shortcuts", authenticateJWT, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { hostId, path } = req.body; if (!isNonEmptyString(userId) || !hostId || !path) { @@ -1224,21 +1225,26 @@ router.delete( }, ); -async function resolveHostCredentials(host: any): Promise { +async function resolveHostCredentials( + host: Record, +): Promise> { try { if (host.credentialId && host.userId) { + const credentialId = host.credentialId as number; + const userId = host.userId as string; + const credentials = await SimpleDBOps.select( db .select() .from(sshCredentials) .where( and( - eq(sshCredentials.id, host.credentialId), - eq(sshCredentials.userId, host.userId), + eq(sshCredentials.id, credentialId), + eq(sshCredentials.userId, userId), ), ), "ssh_credentials", - host.userId, + userId, ); if (credentials.length > 0) { @@ -1277,7 +1283,7 @@ router.put( "/folders/rename", authenticateJWT, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { oldName, newName } = req.body; if (!isNonEmptyString(userId) || !oldName || !newName) { @@ -1342,7 +1348,7 @@ router.post( "/bulk-import", authenticateJWT, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { hosts } = req.body; if (!Array.isArray(hosts) || hosts.length === 0) { @@ -1414,7 +1420,7 @@ router.post( continue; } - const sshDataObj: any = { + const sshDataObj: Record = { userId: userId, name: hostData.name || `${hostData.username}@${hostData.ip}`, folder: hostData.folder || "Default", @@ -1472,7 +1478,7 @@ router.post( authenticateJWT, requireDataAccess, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { sshConfigId } = req.body; if (!sshConfigId || typeof sshConfigId !== "number") { @@ -1536,7 +1542,7 @@ router.post( const tunnelConnections = JSON.parse(config.tunnelConnections); const resolvedConnections = await Promise.all( - tunnelConnections.map(async (tunnel: any) => { + tunnelConnections.map(async (tunnel: Record) => { if ( tunnel.autoStart && tunnel.endpointHost && @@ -1625,7 +1631,7 @@ router.delete( "/autostart/disable", authenticateJWT, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { sshConfigId } = req.body; if (!sshConfigId || typeof sshConfigId !== "number") { @@ -1671,7 +1677,7 @@ router.get( "/autostart/status", authenticateJWT, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; try { const autostartConfigs = await db diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index 92d9451a..eda828b7 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -1,3 +1,4 @@ +import type { AuthenticatedRequest } from "../../../types/index.js"; import express from "express"; import crypto from "crypto"; import { db } from "../db/index.js"; @@ -27,7 +28,7 @@ async function verifyOIDCToken( idToken: string, issuerUrl: string, clientId: string, -): Promise { +): Promise> { const normalizedIssuerUrl = issuerUrl.endsWith("/") ? issuerUrl.slice(0, -1) : issuerUrl; @@ -48,22 +49,25 @@ async function verifyOIDCToken( const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`; const discoveryResponse = await fetch(discoveryUrl); if (discoveryResponse.ok) { - const discovery = (await discoveryResponse.json()) as any; + const discovery = (await discoveryResponse.json()) as Record< + string, + unknown + >; if (discovery.jwks_uri) { - jwksUrls.unshift(discovery.jwks_uri); + jwksUrls.unshift(discovery.jwks_uri as string); } } } catch (discoveryError) { authLogger.error(`OIDC discovery failed: ${discoveryError}`); } - let jwks: any = null; + let jwks: Record | null = null; for (const url of jwksUrls) { try { const response = await fetch(url); if (response.ok) { - const jwksData = (await response.json()) as any; + const jwksData = (await response.json()) as Record; if (jwksData && jwksData.keys && Array.isArray(jwksData.keys)) { jwks = jwksData; break; @@ -95,10 +99,12 @@ async function verifyOIDCToken( ); const keyId = header.kid; - const publicKey = jwks.keys.find((key: any) => key.kid === keyId); + const publicKey = jwks.keys.find( + (key: Record) => key.kid === keyId, + ); if (!publicKey) { throw new Error( - `No matching public key found for key ID: ${keyId}. Available keys: ${jwks.keys.map((k: any) => k.kid).join(", ")}`, + `No matching public key found for key ID: ${keyId}. Available keys: ${jwks.keys.map((k: Record) => k.kid).join(", ")}`, ); } @@ -115,7 +121,7 @@ async function verifyOIDCToken( const router = express.Router(); -function isNonEmptyString(val: any): val is string { +function isNonEmptyString(val: unknown): val is string { return typeof val === "string" && val.trim().length > 0; } @@ -129,7 +135,7 @@ router.post("/create", async (req, res) => { const row = db.$client .prepare("SELECT value FROM settings WHERE key = 'allow_registration'") .get(); - if (row && (row as any).value !== "true") { + if (row && (row as Record).value !== "true") { return res .status(403) .json({ error: "Registration is currently disabled" }); @@ -174,7 +180,7 @@ router.post("/create", async (req, res) => { const countResult = db.$client .prepare("SELECT COUNT(*) as count FROM users") .get(); - isFirstUser = ((countResult as any)?.count || 0) === 0; + isFirstUser = ((countResult as { count?: number })?.count || 0) === 0; const saltRounds = parseInt(process.env.SALT || "10", 10); const password_hash = await bcrypt.hash(password, saltRounds); @@ -238,7 +244,7 @@ router.post("/create", async (req, res) => { // Route: Create OIDC provider configuration (admin only) // POST /users/oidc-config router.post("/oidc-config", authenticateJWT, async (req, res) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; try { const user = await db.select().from(users).where(eq(users.id, userId)); if (!user || user.length === 0 || !user[0].is_admin) { @@ -378,7 +384,7 @@ router.post("/oidc-config", authenticateJWT, async (req, res) => { // Route: Disable OIDC configuration (admin only) // DELETE /users/oidc-config router.delete("/oidc-config", authenticateJWT, async (req, res) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; try { const user = await db.select().from(users).where(eq(users.id, userId)); if (!user || user.length === 0 || !user[0].is_admin) { @@ -408,7 +414,7 @@ router.get("/oidc-config", async (req, res) => { return res.json(null); } - let config = JSON.parse((row as any).value); + let config = JSON.parse((row as Record).value as string); if (config.client_secret) { if (config.client_secret.startsWith("encrypted:")) { @@ -485,7 +491,7 @@ router.get("/oidc/authorize", async (req, res) => { return res.status(404).json({ error: "OIDC not configured" }); } - const config = JSON.parse((row as any).value); + const config = JSON.parse((row as Record).value as string); const state = nanoid(); const nonce = nanoid(); @@ -540,7 +546,8 @@ router.get("/oidc/callback", async (req, res) => { .status(400) .json({ error: "Invalid state parameter - redirect URI not found" }); } - const redirectUri = (storedRedirectRow as any).value; + const redirectUri = (storedRedirectRow as Record) + .value as string; try { const storedNonce = db.$client @@ -564,7 +571,9 @@ router.get("/oidc/callback", async (req, res) => { return res.status(500).json({ error: "OIDC not configured" }); } - const config = JSON.parse((configRow as any).value); + const config = JSON.parse( + (configRow as Record).value as string, + ); const tokenResponse = await fetch(config.token_url, { method: "POST", @@ -590,9 +599,9 @@ router.get("/oidc/callback", async (req, res) => { .json({ error: "Failed to exchange authorization code" }); } - const tokenData = (await tokenResponse.json()) as any; + const tokenData = (await tokenResponse.json()) as Record; - let userInfo: any = null; + let userInfo: Record = null; const userInfoUrls: string[] = []; const normalizedIssuerUrl = config.issuer_url.endsWith("/") @@ -604,9 +613,12 @@ router.get("/oidc/callback", async (req, res) => { const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`; const discoveryResponse = await fetch(discoveryUrl); if (discoveryResponse.ok) { - const discovery = (await discoveryResponse.json()) as any; + const discovery = (await discoveryResponse.json()) as Record< + string, + unknown + >; if (discovery.userinfo_endpoint) { - userInfoUrls.push(discovery.userinfo_endpoint); + userInfoUrls.push(discovery.userinfo_endpoint as string); } } } catch (discoveryError) { @@ -631,14 +643,14 @@ router.get("/oidc/callback", async (req, res) => { if (tokenData.id_token) { try { userInfo = await verifyOIDCToken( - tokenData.id_token, + tokenData.id_token as string, config.issuer_url, config.client_id, ); } catch { // Fallback to manual decoding try { - const parts = tokenData.id_token.split("."); + const parts = (tokenData.id_token as string).split("."); if (parts.length === 3) { const payload = JSON.parse( Buffer.from(parts[1], "base64").toString(), @@ -661,7 +673,10 @@ router.get("/oidc/callback", async (req, res) => { }); if (userInfoResponse.ok) { - userInfo = await userInfoResponse.json(); + userInfo = (await userInfoResponse.json()) as Record< + string, + unknown + >; break; } else { authLogger.error( @@ -684,7 +699,10 @@ router.get("/oidc/callback", async (req, res) => { return res.status(400).json({ error: "Failed to get user information" }); } - const getNestedValue = (obj: any, path: string): any => { + const getNestedValue = ( + obj: Record, + path: string, + ): any => { if (!path || !obj) return null; return path.split(".").reduce((current, key) => current?.[key], obj); }; @@ -725,7 +743,7 @@ router.get("/oidc/callback", async (req, res) => { const countResult = db.$client .prepare("SELECT COUNT(*) as count FROM users") .get(); - isFirstUser = ((countResult as any)?.count || 0) === 0; + isFirstUser = ((countResult as { count?: number })?.count || 0) === 0; const id = nanoid(); await db.insert(users).values({ @@ -787,7 +805,10 @@ router.get("/oidc/callback", async (req, res) => { expiresIn: "50d", }); - let frontendUrl = redirectUri.replace("/users/oidc/callback", ""); + let frontendUrl = (redirectUri as string).replace( + "/users/oidc/callback", + "", + ); if (frontendUrl.includes("localhost")) { frontendUrl = "http://localhost:5173"; @@ -806,7 +827,10 @@ router.get("/oidc/callback", async (req, res) => { } catch (err) { authLogger.error("OIDC callback failed", err); - let frontendUrl = redirectUri.replace("/users/oidc/callback", ""); + let frontendUrl = (redirectUri as string).replace( + "/users/oidc/callback", + "", + ); if (frontendUrl.includes("localhost")) { frontendUrl = "http://localhost:5173"; @@ -931,7 +955,7 @@ router.post("/login", async (req, res) => { dataUnlocked: true, }); - const response: any = { + const response: Record = { success: true, is_admin: !!userRecord.is_admin, username: userRecord.username, @@ -962,7 +986,7 @@ router.post("/login", async (req, res) => { // POST /users/logout router.post("/logout", async (req, res) => { try { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; if (userId) { authManager.logoutUser(userId); @@ -984,7 +1008,7 @@ router.post("/logout", async (req, res) => { // Route: Get current user's info using JWT // GET /users/me router.get("/me", authenticateJWT, async (req: Request, res: Response) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; if (!isNonEmptyString(userId)) { authLogger.warn("Invalid userId in JWT for /users/me"); return res.status(401).json({ error: "Invalid userId" }); @@ -1019,7 +1043,7 @@ router.get("/setup-required", async (req, res) => { const countResult = db.$client .prepare("SELECT COUNT(*) as count FROM users") .get(); - const count = (countResult as any)?.count || 0; + const count = (countResult as { count?: number })?.count || 0; res.json({ setup_required: count === 0, @@ -1033,7 +1057,7 @@ router.get("/setup-required", async (req, res) => { // Route: Count users (admin only - for dashboard statistics) // GET /users/count router.get("/count", authenticateJWT, async (req, res) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; try { const user = await db.select().from(users).where(eq(users.id, userId)); if (!user[0] || !user[0].is_admin) { @@ -1043,7 +1067,7 @@ router.get("/count", authenticateJWT, async (req, res) => { const countResult = db.$client .prepare("SELECT COUNT(*) as count FROM users") .get(); - const count = (countResult as any)?.count || 0; + const count = (countResult as { count?: number })?.count || 0; res.json({ count }); } catch (err) { authLogger.error("Failed to count users", err); @@ -1070,7 +1094,9 @@ router.get("/registration-allowed", async (req, res) => { const row = db.$client .prepare("SELECT value FROM settings WHERE key = 'allow_registration'") .get(); - res.json({ allowed: row ? (row as any).value === "true" : true }); + res.json({ + allowed: row ? (row as Record).value === "true" : true, + }); } catch (err) { authLogger.error("Failed to get registration allowed", err); res.status(500).json({ error: "Failed to get registration allowed" }); @@ -1080,7 +1106,7 @@ router.get("/registration-allowed", async (req, res) => { // Route: Set registration allowed status (admin only) // PATCH /users/registration-allowed router.patch("/registration-allowed", authenticateJWT, async (req, res) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; try { const user = await db.select().from(users).where(eq(users.id, userId)); if (!user || user.length === 0 || !user[0].is_admin) { @@ -1107,7 +1133,9 @@ router.get("/password-login-allowed", async (req, res) => { const row = db.$client .prepare("SELECT value FROM settings WHERE key = 'allow_password_login'") .get(); - res.json({ allowed: row ? (row as { value: string }).value === "true" : true }); + res.json({ + allowed: row ? (row as { value: string }).value === "true" : true, + }); } catch (err) { authLogger.error("Failed to get password login allowed", err); res.status(500).json({ error: "Failed to get password login allowed" }); @@ -1117,7 +1145,7 @@ router.get("/password-login-allowed", async (req, res) => { // Route: Set password login allowed status (admin only) // PATCH /users/password-login-allowed router.patch("/password-login-allowed", authenticateJWT, async (req, res) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; try { const user = await db.select().from(users).where(eq(users.id, userId)); if (!user || user.length === 0 || !user[0].is_admin) { @@ -1128,7 +1156,9 @@ router.patch("/password-login-allowed", authenticateJWT, async (req, res) => { return res.status(400).json({ error: "Invalid value for allowed" }); } db.$client - .prepare("UPDATE settings SET value = ? WHERE key = 'allow_password_login'") + .prepare( + "UPDATE settings SET value = ? WHERE key = 'allow_password_login'", + ) .run(allowed ? "true" : "false"); res.json({ allowed }); } catch (err) { @@ -1140,7 +1170,7 @@ router.patch("/password-login-allowed", authenticateJWT, async (req, res) => { // Route: Delete user account // DELETE /users/delete-account router.delete("/delete-account", authenticateJWT, async (req, res) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { password } = req.body; if (!isNonEmptyString(password)) { @@ -1176,7 +1206,7 @@ router.delete("/delete-account", authenticateJWT, async (req, res) => { const adminCount = db.$client .prepare("SELECT COUNT(*) as count FROM users WHERE is_admin = 1") .get(); - if ((adminCount as any)?.count <= 1) { + if (((adminCount as { count?: number })?.count || 0) <= 1) { return res .status(403) .json({ error: "Cannot delete the last admin user" }); @@ -1266,7 +1296,9 @@ router.post("/verify-reset-code", async (req, res) => { .json({ error: "No reset code found for this user" }); } - const resetData = JSON.parse((resetDataRow as any).value); + const resetData = JSON.parse( + (resetDataRow as Record).value as string, + ); const now = new Date(); const expiresAt = new Date(resetData.expiresAt); @@ -1324,7 +1356,9 @@ router.post("/complete-reset", async (req, res) => { return res.status(400).json({ error: "No temporary token found" }); } - const tempTokenData = JSON.parse((tempTokenRow as any).value); + const tempTokenData = JSON.parse( + (tempTokenRow as Record).value as string, + ); const now = new Date(); const expiresAt = new Date(tempTokenData.expiresAt); @@ -1412,7 +1446,7 @@ router.post("/complete-reset", async (req, res) => { // Route: List all users (admin only) // GET /users/list router.get("/list", authenticateJWT, async (req, res) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; try { const user = await db.select().from(users).where(eq(users.id, userId)); if (!user || user.length === 0 || !user[0].is_admin) { @@ -1438,7 +1472,7 @@ router.get("/list", authenticateJWT, async (req, res) => { // Route: Make user admin (admin only) // POST /users/make-admin router.post("/make-admin", authenticateJWT, async (req, res) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { username } = req.body; if (!isNonEmptyString(username)) { @@ -1481,7 +1515,7 @@ router.post("/make-admin", authenticateJWT, async (req, res) => { // Route: Remove admin status (admin only) // POST /users/remove-admin router.post("/remove-admin", authenticateJWT, async (req, res) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { username } = req.body; if (!isNonEmptyString(username)) { @@ -1638,7 +1672,7 @@ router.post("/totp/verify-login", async (req, res) => { }); } - const response: any = { + const response: Record = { success: true, is_admin: !!userRecord.is_admin, username: userRecord.username, @@ -1668,7 +1702,7 @@ router.post("/totp/verify-login", async (req, res) => { // Route: Setup TOTP // POST /users/totp/setup router.post("/totp/setup", authenticateJWT, async (req, res) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; try { const user = await db.select().from(users).where(eq(users.id, userId)); @@ -1707,7 +1741,7 @@ router.post("/totp/setup", authenticateJWT, async (req, res) => { // Route: Enable TOTP // POST /users/totp/enable router.post("/totp/enable", authenticateJWT, async (req, res) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { totp_code } = req.body; if (!totp_code) { @@ -1766,7 +1800,7 @@ router.post("/totp/enable", authenticateJWT, async (req, res) => { // Route: Disable TOTP // POST /users/totp/disable router.post("/totp/disable", authenticateJWT, async (req, res) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { password, totp_code } = req.body; if (!password && !totp_code) { @@ -1824,7 +1858,7 @@ router.post("/totp/disable", authenticateJWT, async (req, res) => { // Route: Generate new backup codes // POST /users/totp/backup-codes router.post("/totp/backup-codes", authenticateJWT, async (req, res) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { password, totp_code } = req.body; if (!password && !totp_code) { @@ -1882,7 +1916,7 @@ router.post("/totp/backup-codes", authenticateJWT, async (req, res) => { // Route: Delete user (admin only) // DELETE /users/delete-user router.delete("/delete-user", authenticateJWT, async (req, res) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { username } = req.body; if (!isNonEmptyString(username)) { @@ -1911,7 +1945,7 @@ router.delete("/delete-user", authenticateJWT, async (req, res) => { const adminCount = db.$client .prepare("SELECT COUNT(*) as count FROM users WHERE is_admin = 1") .get(); - if ((adminCount as any)?.count <= 1) { + if (((adminCount as { count?: number })?.count || 0) <= 1) { return res .status(403) .json({ error: "Cannot delete the last admin user" }); @@ -1968,7 +2002,7 @@ router.delete("/delete-user", authenticateJWT, async (req, res) => { // Route: User data unlock - used when session expires // POST /users/unlock-data router.post("/unlock-data", authenticateJWT, async (req, res) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { password } = req.body; if (!password) { @@ -2001,7 +2035,7 @@ router.post("/unlock-data", authenticateJWT, async (req, res) => { // Route: Check user data unlock status // GET /users/data-status router.get("/data-status", authenticateJWT, async (req, res) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; try { const isUnlocked = authManager.isUserUnlocked(userId); @@ -2023,7 +2057,7 @@ router.get("/data-status", authenticateJWT, async (req, res) => { // Route: Change user password (re-encrypt data keys) // POST /users/change-password router.post("/change-password", authenticateJWT, async (req, res) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; const { currentPassword, newPassword } = req.body; if (!currentPassword || !newPassword) { diff --git a/src/backend/ssh/server-stats.ts b/src/backend/ssh/server-stats.ts index 42da3d13..399a85ee 100644 --- a/src/backend/ssh/server-stats.ts +++ b/src/backend/ssh/server-stats.ts @@ -9,6 +9,7 @@ import { eq, and } from "drizzle-orm"; import { statsLogger } from "../utils/logger.js"; import { SimpleDBOps } from "../utils/simple-db-ops.js"; import { AuthManager } from "../utils/auth-manager.js"; +import type { AuthenticatedRequest } from "../../types/index.js"; interface PooledConnection { client: Client; @@ -237,7 +238,7 @@ class RequestQueue { } interface CachedMetrics { - data: any; + data: unknown; timestamp: number; hostId: number; } @@ -246,7 +247,7 @@ class MetricsCache { private cache = new Map(); private ttl = 30000; - get(hostId: number): any | null { + get(hostId: number): unknown | null { const cached = this.cache.get(hostId); if (cached && Date.now() - cached.timestamp < this.ttl) { return cached.data; @@ -254,7 +255,7 @@ class MetricsCache { return null; } - set(hostId: number, data: any): void { + set(hostId: number, data: unknown): void { this.cache.set(hostId, { data, timestamp: Date.now(), @@ -297,7 +298,7 @@ interface SSHHostWithCredentials { enableTunnel: boolean; enableFileManager: boolean; defaultPath: string; - tunnelConnections: any[]; + tunnelConnections: unknown[]; statsConfig?: string; createdAt: string; updatedAt: string; @@ -432,11 +433,11 @@ async function fetchHostById( } async function resolveHostCredentials( - host: any, + host: Record, userId: string, ): Promise { try { - const baseHost: any = { + const baseHost: Record = { id: host.id, name: host.name, ip: host.ip, @@ -456,7 +457,7 @@ async function resolveHostCredentials( enableFileManager: !!host.enableFileManager, defaultPath: host.defaultPath || "/", tunnelConnections: host.tunnelConnections - ? JSON.parse(host.tunnelConnections) + ? JSON.parse(host.tunnelConnections as string) : [], statsConfig: host.statsConfig || undefined, createdAt: host.createdAt, @@ -472,7 +473,7 @@ async function resolveHostCredentials( .from(sshCredentials) .where( and( - eq(sshCredentials.id, host.credentialId), + eq(sshCredentials.id, host.credentialId as number), eq(sshCredentials.userId, userId), ), ), @@ -512,7 +513,7 @@ async function resolveHostCredentials( addLegacyCredentials(baseHost, host); } - return baseHost; + return baseHost as unknown as SSHHostWithCredentials; } catch (error) { statsLogger.error( `Failed to resolve host credentials for host ${host.id}: ${error instanceof Error ? error.message : "Unknown error"}`, @@ -521,7 +522,10 @@ async function resolveHostCredentials( } } -function addLegacyCredentials(baseHost: any, host: any): void { +function addLegacyCredentials( + baseHost: Record, + host: Record, +): void { baseHost.password = host.password || null; baseHost.key = host.key || null; baseHost.keyPassword = host.key_password || host.keyPassword || null; @@ -573,7 +577,7 @@ function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig { if (!host.password) { throw new Error(`No password available for host ${host.ip}`); } - (base as any).password = host.password; + (base as Record).password = host.password; } else if (host.authType === "key") { if (!host.key) { throw new Error(`No SSH key available for host ${host.ip}`); @@ -589,10 +593,13 @@ function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig { .replace(/\r\n/g, "\n") .replace(/\r/g, "\n"); - (base as any).privateKey = Buffer.from(cleanKey, "utf8"); + (base as Record).privateKey = Buffer.from( + cleanKey, + "utf8", + ); if (host.keyPassword) { - (base as any).passphrase = host.keyPassword; + (base as Record).passphrase = host.keyPassword; } } catch (keyError) { statsLogger.error( @@ -724,7 +731,9 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{ }> { const cached = metricsCache.get(host.id); if (cached) { - return cached; + return cached as ReturnType extends Promise + ? T + : never; } return requestQueue.queueRequest(host.id, async () => { @@ -873,7 +882,7 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{ } // Collect network interfaces - let interfaces: Array<{ + const interfaces: Array<{ name: string; ip: string; state: string; @@ -958,7 +967,7 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{ // Collect process information let totalProcesses: number | null = null; let runningProcesses: number | null = null; - let topProcesses: Array<{ + const topProcesses: Array<{ pid: string; user: string; cpu: string; @@ -1145,7 +1154,7 @@ async function pollStatusesOnce(userId?: string): Promise { } app.get("/status", async (req, res) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; if (!SimpleDBOps.isUserDataUnlocked(userId)) { return res.status(401).json({ @@ -1166,7 +1175,7 @@ app.get("/status", async (req, res) => { app.get("/status/:id", validateHostId, async (req, res) => { const id = Number(req.params.id); - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; if (!SimpleDBOps.isUserDataUnlocked(userId)) { return res.status(401).json({ @@ -1197,7 +1206,7 @@ app.get("/status/:id", validateHostId, async (req, res) => { }); app.post("/refresh", async (req, res) => { - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; if (!SimpleDBOps.isUserDataUnlocked(userId)) { return res.status(401).json({ @@ -1212,7 +1221,7 @@ app.post("/refresh", async (req, res) => { app.get("/metrics/:id", validateHostId, async (req, res) => { const id = Number(req.params.id); - const userId = (req as any).userId; + const userId = (req as AuthenticatedRequest).userId; if (!SimpleDBOps.isUserDataUnlocked(userId)) { return res.status(401).json({ diff --git a/src/backend/ssh/terminal.ts b/src/backend/ssh/terminal.ts index c60eb0ce..d94125aa 100644 --- a/src/backend/ssh/terminal.ts +++ b/src/backend/ssh/terminal.ts @@ -403,12 +403,19 @@ wss.on("connection", async (ws: WebSocket, req) => { if (credentials.length > 0) { const credential = credentials[0]; resolvedCredentials = { - password: credential.password, - key: - credential.private_key || credential.privateKey || credential.key, - keyPassword: credential.key_password || credential.keyPassword, - keyType: credential.key_type || credential.keyType, - authType: credential.auth_type || credential.authType, + password: credential.password as string | undefined, + key: (credential.private_key || + credential.privateKey || + credential.key) as string | undefined, + keyPassword: (credential.key_password || credential.keyPassword) as + | string + | undefined, + keyType: (credential.key_type || credential.keyType) as + | string + | undefined, + authType: (credential.auth_type || credential.authType) as + | string + | undefined, }; } else { sshLogger.warn(`No credentials found for host ${id}`, { @@ -617,13 +624,18 @@ wss.on("connection", async (ws: WebSocket, req) => { ); } else { if (resolvedCredentials.password) { - const responses = prompts.map(() => resolvedCredentials.password || ""); + const responses = prompts.map( + () => resolvedCredentials.password || "", + ); finish(responses); } else { - sshLogger.warn("Keyboard-interactive requires password but none available", { - operation: "ssh_keyboard_interactive_no_password", - hostId: id, - }); + sshLogger.warn( + "Keyboard-interactive requires password but none available", + { + operation: "ssh_keyboard_interactive_no_password", + hostId: id, + }, + ); finish(prompts.map(() => "")); } } diff --git a/src/backend/ssh/tunnel.ts b/src/backend/ssh/tunnel.ts index b652d857..361555ce 100644 --- a/src/backend/ssh/tunnel.ts +++ b/src/backend/ssh/tunnel.ts @@ -515,12 +515,17 @@ async function connectSSHTunnel( if (credentials.length > 0) { const credential = credentials[0]; resolvedSourceCredentials = { - password: credential.password, - sshKey: - credential.private_key || credential.privateKey || credential.key, - keyPassword: credential.key_password || credential.keyPassword, - keyType: credential.key_type || credential.keyType, - authMethod: credential.auth_type || credential.authType, + password: credential.password as string | undefined, + sshKey: (credential.private_key || + credential.privateKey || + credential.key) as string | undefined, + keyPassword: (credential.key_password || credential.keyPassword) as + | string + | undefined, + keyType: (credential.key_type || credential.keyType) as + | string + | undefined, + authMethod: (credential.auth_type || credential.authType) as string, }; } } @@ -593,12 +598,17 @@ async function connectSSHTunnel( if (credentials.length > 0) { const credential = credentials[0]; resolvedEndpointCredentials = { - password: credential.password, - sshKey: - credential.private_key || credential.privateKey || credential.key, - keyPassword: credential.key_password || credential.keyPassword, - keyType: credential.key_type || credential.keyType, - authMethod: credential.auth_type || credential.authType, + password: credential.password as string | undefined, + sshKey: (credential.private_key || + credential.privateKey || + credential.key) as string | undefined, + keyPassword: (credential.key_password || credential.keyPassword) as + | string + | undefined, + keyType: (credential.key_type || credential.keyType) as + | string + | undefined, + authMethod: (credential.auth_type || credential.authType) as string, }; } else { tunnelLogger.warn("No endpoint credentials found in database", { @@ -1031,12 +1041,17 @@ async function killRemoteTunnelByMarker( if (credentials.length > 0) { const credential = credentials[0]; resolvedSourceCredentials = { - password: credential.password, - sshKey: - credential.private_key || credential.privateKey || credential.key, - keyPassword: credential.key_password || credential.keyPassword, - keyType: credential.key_type || credential.keyType, - authMethod: credential.auth_type || credential.authType, + password: credential.password as string | undefined, + sshKey: (credential.private_key || + credential.privateKey || + credential.key) as string | undefined, + keyPassword: (credential.key_password || credential.keyPassword) as + | string + | undefined, + keyType: (credential.key_type || credential.keyType) as + | string + | undefined, + authMethod: (credential.auth_type || credential.authType) as string, }; } } diff --git a/src/backend/utils/data-crypto.ts b/src/backend/utils/data-crypto.ts index 870d0d5f..88fb655a 100644 --- a/src/backend/utils/data-crypto.ts +++ b/src/backend/utils/data-crypto.ts @@ -12,7 +12,7 @@ class DataCrypto { static encryptRecord( tableName: string, - record: any, + record: Record, userId: string, userDataKey: Buffer, ): any { @@ -24,7 +24,7 @@ class DataCrypto { encryptedRecord[fieldName] = FieldCrypto.encryptField( value as string, userDataKey, - recordId, + recordId as string, fieldName, ); } @@ -35,7 +35,7 @@ class DataCrypto { static decryptRecord( tableName: string, - record: any, + record: Record, userId: string, userDataKey: Buffer, ): any { @@ -49,7 +49,7 @@ class DataCrypto { decryptedRecord[fieldName] = LazyFieldEncryption.safeGetFieldValue( value as string, userDataKey, - recordId, + recordId as string, fieldName, ); } @@ -60,13 +60,18 @@ class DataCrypto { static decryptRecords( tableName: string, - records: any[], + records: unknown[], userId: string, userDataKey: Buffer, - ): any[] { + ): unknown[] { if (!Array.isArray(records)) return records; return records.map((record) => - this.decryptRecord(tableName, record, userId, userDataKey), + this.decryptRecord( + tableName, + record as Record, + userId, + userDataKey, + ), ); } @@ -386,7 +391,7 @@ class DataCrypto { static encryptRecordForUser( tableName: string, - record: any, + record: Record, userId: string, ): any { const userDataKey = this.validateUserAccess(userId); @@ -395,7 +400,7 @@ class DataCrypto { static decryptRecordForUser( tableName: string, - record: any, + record: Record, userId: string, ): any { const userDataKey = this.validateUserAccess(userId); @@ -404,9 +409,9 @@ class DataCrypto { static decryptRecordsForUser( tableName: string, - records: any[], + records: unknown[], userId: string, - ): any[] { + ): unknown[] { const userDataKey = this.validateUserAccess(userId); return this.decryptRecords(tableName, records, userId, userDataKey); } diff --git a/src/backend/utils/simple-db-ops.ts b/src/backend/utils/simple-db-ops.ts index 933a3a5e..4d67908b 100644 --- a/src/backend/utils/simple-db-ops.ts +++ b/src/backend/utils/simple-db-ops.ts @@ -5,7 +5,8 @@ import type { SQLiteTable } from "drizzle-orm/sqlite-core"; type TableName = "users" | "ssh_data" | "ssh_credentials"; class SimpleDBOps { - static async insert>( + static async insert>( + // eslint-disable-next-line @typescript-eslint/no-explicit-any table: SQLiteTable, tableName: TableName, data: T, @@ -44,8 +45,8 @@ class SimpleDBOps { return decryptedResult as T; } - static async select>( - query: any, + static async select>( + query: unknown, tableName: TableName, userId: string, ): Promise { @@ -58,16 +59,16 @@ class SimpleDBOps { const decryptedResults = DataCrypto.decryptRecords( tableName, - results, + results as unknown[], userId, userDataKey, ); - return decryptedResults; + return decryptedResults as T[]; } - static async selectOne>( - query: any, + static async selectOne>( + query: unknown, tableName: TableName, userId: string, ): Promise { @@ -81,7 +82,7 @@ class SimpleDBOps { const decryptedResult = DataCrypto.decryptRecord( tableName, - result, + result as Record, userId, userDataKey, ); @@ -89,10 +90,11 @@ class SimpleDBOps { return decryptedResult; } - static async update>( + static async update>( + // eslint-disable-next-line @typescript-eslint/no-explicit-any table: SQLiteTable, tableName: TableName, - where: any, + where: unknown, data: Partial, userId: string, ): Promise { @@ -108,7 +110,8 @@ class SimpleDBOps { const result = await getDb() .update(table) .set(encryptedData) - .where(where) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .where(where as any) .returning(); DatabaseSaveTrigger.triggerSave(`update_${tableName}`); @@ -124,12 +127,17 @@ class SimpleDBOps { } static async delete( + // eslint-disable-next-line @typescript-eslint/no-explicit-any table: SQLiteTable, tableName: TableName, - where: any, + where: unknown, _userId: string, - ): Promise { - const result = await getDb().delete(table).where(where).returning(); + ): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await getDb() + .delete(table) + .where(where as any) + .returning(); DatabaseSaveTrigger.triggerSave(`delete_${tableName}`); @@ -145,12 +153,12 @@ class SimpleDBOps { } static async selectEncrypted( - query: any, + query: unknown, _tableName: TableName, - ): Promise { + ): Promise { const results = await query; - return results; + return results as unknown[]; } } diff --git a/src/backend/utils/user-data-export.ts b/src/backend/utils/user-data-export.ts index 82d3fde3..03c3fff3 100644 --- a/src/backend/utils/user-data-export.ts +++ b/src/backend/utils/user-data-export.ts @@ -18,14 +18,14 @@ interface UserExportData { userId: string; username: string; userData: { - sshHosts: any[]; - sshCredentials: any[]; + sshHosts: unknown[]; + sshCredentials: unknown[]; fileManagerData: { - recent: any[]; - pinned: any[]; - shortcuts: any[]; + recent: unknown[]; + pinned: unknown[]; + shortcuts: unknown[]; }; - dismissedAlerts: any[]; + dismissedAlerts: unknown[]; }; metadata: { totalRecords: number; @@ -83,7 +83,7 @@ class UserDataExport { ) : sshHosts; - let sshCredentialsData: any[] = []; + let sshCredentialsData: unknown[] = []; if (includeCredentials) { const credentials = await getDb() .select() @@ -185,7 +185,10 @@ class UserDataExport { return JSON.stringify(exportData, null, pretty ? 2 : 0); } - static validateExportData(data: any): { valid: boolean; errors: string[] } { + static validateExportData(data: unknown): { + valid: boolean; + errors: string[]; + } { const errors: string[] = []; if (!data || typeof data !== "object") { @@ -193,23 +196,26 @@ class UserDataExport { return { valid: false, errors }; } - if (!data.version) { + const dataObj = data as Record; + + if (!dataObj.version) { errors.push("Missing version field"); } - if (!data.userId) { + if (!dataObj.userId) { errors.push("Missing userId field"); } - if (!data.userData || typeof data.userData !== "object") { + if (!dataObj.userData || typeof dataObj.userData !== "object") { errors.push("Missing or invalid userData field"); } - if (!data.metadata || typeof data.metadata !== "object") { + if (!dataObj.metadata || typeof dataObj.metadata !== "object") { errors.push("Missing or invalid metadata field"); } - if (data.userData) { + if (dataObj.userData) { + const userData = dataObj.userData as Record; const requiredFields = [ "sshHosts", "sshCredentials", @@ -218,23 +224,24 @@ class UserDataExport { ]; for (const field of requiredFields) { if ( - !Array.isArray(data.userData[field]) && - !( - field === "fileManagerData" && - typeof data.userData[field] === "object" - ) + !Array.isArray(userData[field]) && + !(field === "fileManagerData" && typeof userData[field] === "object") ) { errors.push(`Missing or invalid userData.${field} field`); } } if ( - data.userData.fileManagerData && - typeof data.userData.fileManagerData === "object" + userData.fileManagerData && + typeof userData.fileManagerData === "object" ) { + const fileManagerData = userData.fileManagerData as Record< + string, + unknown + >; const fmFields = ["recent", "pinned", "shortcuts"]; for (const field of fmFields) { - if (!Array.isArray(data.userData.fileManagerData[field])) { + if (!Array.isArray(fileManagerData[field])) { errors.push( `Missing or invalid userData.fileManagerData.${field} field`, ); diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 6a544f20..ec3321e4 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -1,22 +1,49 @@ +interface ServerConfig { + serverUrl?: string; + [key: string]: unknown; +} + +interface ConnectionTestResult { + success: boolean; + error?: string; + [key: string]: unknown; +} + +interface DialogOptions { + title?: string; + defaultPath?: string; + buttonLabel?: string; + filters?: Array<{ name: string; extensions: string[] }>; + properties?: string[]; + [key: string]: unknown; +} + +interface DialogResult { + canceled: boolean; + filePath?: string; + filePaths?: string[]; + [key: string]: unknown; +} + export interface ElectronAPI { getAppVersion: () => Promise; getPlatform: () => Promise; - getServerConfig: () => Promise; - saveServerConfig: (config: any) => Promise; - testServerConnection: (serverUrl: string) => Promise; + getServerConfig: () => Promise; + saveServerConfig: (config: ServerConfig) => Promise<{ success: boolean }>; + testServerConnection: (serverUrl: string) => Promise; - showSaveDialog: (options: any) => Promise; - showOpenDialog: (options: any) => Promise; + showSaveDialog: (options: DialogOptions) => Promise; + showOpenDialog: (options: DialogOptions) => Promise; - onUpdateAvailable: (callback: Function) => void; - onUpdateDownloaded: (callback: Function) => void; + onUpdateAvailable: (callback: () => void) => void; + onUpdateDownloaded: (callback: () => void) => void; removeAllListeners: (channel: string) => void; isElectron: boolean; isDev: boolean; - invoke: (channel: string, ...args: any[]) => Promise; + invoke: (channel: string, ...args: unknown[]) => Promise; createTempFile: (fileData: { fileName: string; diff --git a/src/types/index.ts b/src/types/index.ts index 102d7f01..effac75c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -4,6 +4,7 @@ // This file contains all shared interfaces and types used across the application import type { Client } from "ssh2"; +import type { Request } from "express"; // ============================================================================ // SSH HOST TYPES @@ -58,7 +59,7 @@ export interface SSHHostData { enableTunnel?: boolean; enableFileManager?: boolean; defaultPath?: string; - tunnelConnections?: any[]; + tunnelConnections?: TunnelConnection[]; statsConfig?: string; } @@ -263,8 +264,8 @@ export interface TabContextTab { | "file_manager" | "user_profile"; title: string; - hostConfig?: any; - terminalRef?: React.RefObject; + hostConfig?: SSHHost; + terminalRef?: React.RefObject; } // ============================================================================ @@ -305,7 +306,7 @@ export type KeyType = "rsa" | "ecdsa" | "ed25519"; // API RESPONSE TYPES // ============================================================================ -export interface ApiResponse { +export interface ApiResponse { data?: T; error?: string; message?: string; @@ -368,13 +369,13 @@ export interface SSHTunnelViewerProps { action: "connect" | "disconnect" | "cancel", host: SSHHost, tunnelIndex: number, - ) => Promise + ) => Promise >; onTunnelAction?: ( action: "connect" | "disconnect" | "cancel", host: SSHHost, tunnelIndex: number, - ) => Promise; + ) => Promise; } export interface FileManagerProps { @@ -402,7 +403,7 @@ export interface SSHTunnelObjectProps { action: "connect" | "disconnect" | "cancel", host: SSHHost, tunnelIndex: number, - ) => Promise; + ) => Promise; compact?: boolean; bare?: boolean; } @@ -461,3 +462,95 @@ export type Optional = Omit & Partial>; export type RequiredFields = T & Required>; export type PartialExcept = Partial & Pick; + +// ============================================================================ +// EXPRESS REQUEST TYPES +// ============================================================================ + +export interface AuthenticatedRequest extends Request { + userId: string; + user?: { + id: string; + username: string; + isAdmin: boolean; + }; +} + +// ============================================================================ +// GITHUB API TYPES +// ============================================================================ + +export interface GitHubAsset { + id: number; + name: string; + size: number; + download_count: number; + browser_download_url: string; +} + +export interface GitHubRelease { + id: number; + tag_name: string; + name: string; + body: string; + published_at: string; + html_url: string; + assets: GitHubAsset[]; + prerelease: boolean; + draft: boolean; +} + +export interface GitHubAPIResponse { + data: T; + cached: boolean; + cache_age?: number; + timestamp?: number; +} + +// ============================================================================ +// CACHE TYPES +// ============================================================================ + +export interface CacheEntry { + data: T; + timestamp: number; + expiresAt: number; +} + +// ============================================================================ +// DATABASE EXPORT/IMPORT TYPES +// ============================================================================ + +export interface ExportSummary { + sshHostsImported: number; + sshCredentialsImported: number; + fileManagerItemsImported: number; + dismissedAlertsImported: number; + credentialUsageImported: number; + settingsImported: number; + skippedItems: number; + errors: string[]; +} + +export interface ImportResult { + success: boolean; + summary: ExportSummary; +} + +export interface ExportRequestBody { + password: string; +} + +export interface ImportRequestBody { + password: string; +} + +export interface ExportPreviewBody { + scope?: string; + includeCredentials?: boolean; +} + +export interface RestoreRequestBody { + backupPath: string; + targetPath?: string; +} diff --git a/src/ui/Desktop/Admin/AdminSettings.tsx b/src/ui/Desktop/Admin/AdminSettings.tsx index 50f4ec6a..eb50e10b 100644 --- a/src/ui/Desktop/Admin/AdminSettings.tsx +++ b/src/ui/Desktop/Admin/AdminSettings.tsx @@ -107,7 +107,8 @@ export function AdminSettings({ React.useEffect(() => { if (isElectron()) { - const serverUrl = (window as any).configuredServerUrl; + const serverUrl = (window as { configuredServerUrl?: string }) + .configuredServerUrl; if (!serverUrl) { return; } @@ -127,7 +128,8 @@ export function AdminSettings({ React.useEffect(() => { if (isElectron()) { - const serverUrl = (window as any).configuredServerUrl; + const serverUrl = (window as { configuredServerUrl?: string }) + .configuredServerUrl; if (!serverUrl) { return; } @@ -148,7 +150,8 @@ export function AdminSettings({ React.useEffect(() => { if (isElectron()) { - const serverUrl = (window as any).configuredServerUrl; + const serverUrl = (window as { configuredServerUrl?: string }) + .configuredServerUrl; if (!serverUrl) { return; } @@ -169,7 +172,8 @@ export function AdminSettings({ const fetchUsers = async () => { if (isElectron()) { - const serverUrl = (window as any).configuredServerUrl; + const serverUrl = (window as { configuredServerUrl?: string }) + .configuredServerUrl; if (!serverUrl) { return; } @@ -234,9 +238,10 @@ export function AdminSettings({ try { await updateOIDCConfig(oidcConfig); toast.success(t("admin.oidcConfigurationUpdated")); - } catch (err: any) { + } catch (err: unknown) { setOidcError( - err?.response?.data?.error || t("admin.failedToUpdateOidcConfig"), + (err as { response?: { data?: { error?: string } } })?.response?.data + ?.error || t("admin.failedToUpdateOidcConfig"), ); } finally { setOidcLoading(false); @@ -257,9 +262,10 @@ export function AdminSettings({ toast.success(t("admin.userIsNowAdmin", { username: newAdminUsername })); setNewAdminUsername(""); fetchUsers(); - } catch (err: any) { + } catch (err: unknown) { setMakeAdminError( - err?.response?.data?.error || t("admin.failedToMakeUserAdmin"), + (err as { response?: { data?: { error?: string } } })?.response?.data + ?.error || t("admin.failedToMakeUserAdmin"), ); } finally { setMakeAdminLoading(false); @@ -272,7 +278,7 @@ export function AdminSettings({ await removeAdminStatus(username); toast.success(t("admin.adminStatusRemoved", { username })); fetchUsers(); - } catch (err: any) { + } catch (err: unknown) { toast.error(t("admin.failedToRemoveAdminStatus")); } }); @@ -286,7 +292,7 @@ export function AdminSettings({ await deleteUser(username); toast.success(t("admin.userDeletedSuccessfully", { username })); fetchUsers(); - } catch (err: any) { + } catch (err: unknown) { toast.error(t("admin.failedToDeleteUser")); } }, @@ -316,7 +322,7 @@ export function AdminSettings({ window.location.hostname === "127.0.0.1"); const apiUrl = isElectron() - ? `${(window as any).configuredServerUrl}/database/export` + ? `${(window as { configuredServerUrl?: string }).configuredServerUrl}/database/export` : isDev ? `http://localhost:30001/database/export` : `${window.location.protocol}//${window.location.host}/database/export`; @@ -386,7 +392,7 @@ export function AdminSettings({ window.location.hostname === "127.0.0.1"); const apiUrl = isElectron() - ? `${(window as any).configuredServerUrl}/database/import` + ? `${(window as { configuredServerUrl?: string }).configuredServerUrl}/database/import` : isDev ? `http://localhost:30001/database/import` : `${window.location.protocol}//${window.location.host}/database/import`; @@ -713,9 +719,13 @@ export function AdminSettings({ try { await disableOIDCConfig(); toast.success(t("admin.oidcConfigurationDisabled")); - } catch (err: any) { + } catch (err: unknown) { setOidcError( - err?.response?.data?.error || + ( + err as { + response?: { data?: { error?: string } }; + } + )?.response?.data?.error || t("admin.failedToDisableOidcConfig"), ); } finally { diff --git a/src/ui/Desktop/Apps/File Manager/FileManager.tsx b/src/ui/Desktop/Apps/File Manager/FileManager.tsx index 0945fa88..e6319df7 100644 --- a/src/ui/Desktop/Apps/File Manager/FileManager.tsx +++ b/src/ui/Desktop/Apps/File Manager/FileManager.tsx @@ -311,10 +311,10 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { setFiles(files); clearSelection(); initialLoadDoneRef.current = true; - } catch (dirError: any) { + } catch (dirError: unknown) { console.error("Failed to load initial directory:", dirError); } - } catch (error: any) { + } catch (error: unknown) { console.error("SSH connection failed:", error); handleCloseWithError( t("fileManager.failedToConnect") + ": " + (error.message || error), @@ -353,7 +353,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { setFiles(files); clearSelection(); - } catch (error: any) { + } catch (error: unknown) { if (currentLoadingPathRef.current === path) { console.error("Failed to load directory:", error); @@ -535,7 +535,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { t("fileManager.fileUploadedSuccessfully", { name: file.name }), ); handleRefreshDirectory(); - } catch (error: any) { + } catch (error: unknown) { toast.dismiss(progressToast); if ( @@ -584,7 +584,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { t("fileManager.fileDownloadedSuccessfully", { name: file.name }), ); } - } catch (error: any) { + } catch (error: unknown) { if ( error.message?.includes("connection") || error.message?.includes("established") @@ -665,7 +665,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { ); handleRefreshDirectory(); clearSelection(); - } catch (error: any) { + } catch (error: unknown) { if ( error.message?.includes("connection") || error.message?.includes("established") @@ -775,7 +775,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { component: createWindowComponent, }); } - } catch (error: any) { + } catch (error: unknown) { toast.error( error?.response?.data?.error || error?.message || @@ -914,7 +914,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { successCount++; } } - } catch (error: any) { + } catch (error: unknown) { console.error(`Failed to ${operation} file ${file.name}:`, error); toast.error( t("fileManager.operationFailed", { @@ -1015,7 +1015,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { if (operation === "cut") { setClipboard(null); } - } catch (error: any) { + } catch (error: unknown) { toast.error( `${t("fileManager.pasteFailed")}: ${error.message || t("fileManager.unknownError")}`, ); @@ -1050,7 +1050,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { currentHost?.userId?.toString(), ); successCount++; - } catch (error: any) { + } catch (error: unknown) { console.error( `Failed to delete copied file ${copiedFile.targetName}:`, error, @@ -1092,7 +1092,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { currentHost?.userId?.toString(), ); successCount++; - } catch (error: any) { + } catch (error: unknown) { console.error( `Failed to move back file ${movedFile.targetName}:`, error, @@ -1132,7 +1132,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { } handleRefreshDirectory(); - } catch (error: any) { + } catch (error: unknown) { toast.error( `${t("fileManager.undoOperationFailed")}: ${error.message || t("fileManager.unknownError")}`, ); @@ -1204,7 +1204,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { setCreateIntent(null); handleRefreshDirectory(); - } catch (error: any) { + } catch (error: unknown) { console.error("Create failed:", error); toast.error(t("fileManager.failedToCreateItem")); } @@ -1233,7 +1233,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { ); setEditingFile(null); handleRefreshDirectory(); - } catch (error: any) { + } catch (error: unknown) { console.error("Rename failed:", error); toast.error(t("fileManager.failedToRenameItem")); } @@ -1269,11 +1269,11 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { clearSelection(); initialLoadDoneRef.current = true; toast.success(t("fileManager.connectedSuccessfully")); - } catch (dirError: any) { + } catch (dirError: unknown) { console.error("Failed to load initial directory:", dirError); } } - } catch (error: any) { + } catch (error: unknown) { console.error("TOTP verification failed:", error); toast.error(t("fileManager.totpVerificationFailed")); } finally { @@ -1340,7 +1340,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { movedItems.push(file.name); successCount++; } - } catch (error: any) { + } catch (error: unknown) { console.error(`Failed to move file ${file.name}:`, error); toast.error( t("fileManager.moveFileFailed", { name: file.name }) + @@ -1388,7 +1388,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { handleRefreshDirectory(); clearSelection(); } - } catch (error: any) { + } catch (error: unknown) { console.error("Drag move operation failed:", error); toast.error(t("fileManager.moveOperationFailed") + ": " + error.message); } @@ -1459,7 +1459,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { await dragToDesktop.dragFilesToDesktop(files); } } - } catch (error: any) { + } catch (error: unknown) { console.error("Drag to desktop failed:", error); toast.error( t("fileManager.dragFailed") + @@ -1554,7 +1554,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { try { const pinnedData = await getPinnedFiles(currentHost.id); - const pinnedPaths = new Set(pinnedData.map((item: any) => item.path)); + const pinnedPaths = new Set( + pinnedData.map((item: Record) => item.path), + ); setPinnedFiles(pinnedPaths); } catch (error) { console.error("Failed to load pinned files:", error); diff --git a/src/ui/Desktop/Apps/Terminal/Terminal.tsx b/src/ui/Desktop/Apps/Terminal/Terminal.tsx index dd6f72fd..0aae78e6 100644 --- a/src/ui/Desktop/Apps/Terminal/Terminal.tsx +++ b/src/ui/Desktop/Apps/Terminal/Terminal.tsx @@ -15,8 +15,30 @@ import { toast } from "sonner"; import { getCookie, isElectron } from "@/ui/main-axios.ts"; import { TOTPDialog } from "@/ui/components/TOTPDialog"; +interface HostConfig { + id?: number; + ip: string; + port: number; + username: string; + password?: string; + key?: string; + keyPassword?: string; + keyType?: string; + authType?: string; + credentialId?: number; + [key: string]: unknown; +} + +interface TerminalHandle { + disconnect: () => void; + fit: () => void; + sendInput: (data: string) => void; + notifyResize: () => void; + refresh: () => void; +} + interface SSHTerminalProps { - hostConfig: any; + hostConfig: HostConfig; isVisible: boolean; title?: string; showTitle?: boolean; @@ -26,754 +48,772 @@ interface SSHTerminalProps { executeCommand?: string; } -export const Terminal = forwardRef(function SSHTerminal( - { - hostConfig, - isVisible, - splitScreen = false, - onClose, - initialPath, - executeCommand, - }, - ref, -) { - if (typeof window !== "undefined" && !(window as any).testJWT) { - (window as any).testJWT = () => { - const jwt = getCookie("jwt"); - return jwt; - }; - } - - const { t } = useTranslation(); - const { instance: terminal, ref: xtermRef } = useXTerm(); - const fitAddonRef = useRef(null); - const webSocketRef = useRef(null); - const resizeTimeout = useRef(null); - const wasDisconnectedBySSH = useRef(false); - const pingIntervalRef = useRef(null); - const [visible, setVisible] = useState(false); - const [isConnected, setIsConnected] = useState(false); - const [isConnecting, setIsConnecting] = useState(false); - const [connectionError, setConnectionError] = useState(null); - const [isAuthenticated, setIsAuthenticated] = useState(false); - const [totpRequired, setTotpRequired] = useState(false); - const [totpPrompt, setTotpPrompt] = useState(""); - const isVisibleRef = useRef(false); - const reconnectTimeoutRef = useRef(null); - const reconnectAttempts = useRef(0); - const maxReconnectAttempts = 3; - const isUnmountingRef = useRef(false); - const shouldNotReconnectRef = useRef(false); - const isReconnectingRef = useRef(false); - const isConnectingRef = useRef(false); - const connectionTimeoutRef = useRef(null); - - const lastSentSizeRef = useRef<{ cols: number; rows: number } | null>(null); - const pendingSizeRef = useRef<{ cols: number; rows: number } | null>(null); - const notifyTimerRef = useRef(null); - const DEBOUNCE_MS = 140; - - useEffect(() => { - isVisibleRef.current = isVisible; - }, [isVisible]); - - useEffect(() => { - const checkAuth = () => { - const jwtToken = getCookie("jwt"); - const isAuth = !!(jwtToken && jwtToken.trim() !== ""); - - setIsAuthenticated((prev) => { - if (prev !== isAuth) { - return isAuth; - } - return prev; - }); - }; - - checkAuth(); - - const authCheckInterval = setInterval(checkAuth, 5000); - - return () => clearInterval(authCheckInterval); - }, []); - - function hardRefresh() { - try { - if (terminal && typeof (terminal as any).refresh === "function") { - (terminal as any).refresh(0, terminal.rows - 1); - } - } catch { - // Ignore terminal refresh errors +export const Terminal = forwardRef( + function SSHTerminal( + { + hostConfig, + isVisible, + splitScreen = false, + onClose, + initialPath, + executeCommand, + }, + ref, + ) { + if ( + typeof window !== "undefined" && + !(window as { testJWT?: () => string | null }).testJWT + ) { + (window as { testJWT?: () => string | null }).testJWT = () => { + const jwt = getCookie("jwt"); + return jwt; + }; } - } - function handleTotpSubmit(code: string) { - if (webSocketRef.current && code) { - webSocketRef.current.send( - JSON.stringify({ - type: "totp_response", - data: { code }, - }), - ); + const { t } = useTranslation(); + const { instance: terminal, ref: xtermRef } = useXTerm(); + const fitAddonRef = useRef(null); + const webSocketRef = useRef(null); + const resizeTimeout = useRef(null); + const wasDisconnectedBySSH = useRef(false); + const pingIntervalRef = useRef(null); + const [visible, setVisible] = useState(false); + const [isConnected, setIsConnected] = useState(false); + const [isConnecting, setIsConnecting] = useState(false); + const [connectionError, setConnectionError] = useState(null); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [totpRequired, setTotpRequired] = useState(false); + const [totpPrompt, setTotpPrompt] = useState(""); + const isVisibleRef = useRef(false); + const reconnectTimeoutRef = useRef(null); + const reconnectAttempts = useRef(0); + const maxReconnectAttempts = 3; + const isUnmountingRef = useRef(false); + const shouldNotReconnectRef = useRef(false); + const isReconnectingRef = useRef(false); + const isConnectingRef = useRef(false); + const connectionTimeoutRef = useRef(null); + + const lastSentSizeRef = useRef<{ cols: number; rows: number } | null>(null); + const pendingSizeRef = useRef<{ cols: number; rows: number } | null>(null); + const notifyTimerRef = useRef(null); + const DEBOUNCE_MS = 140; + + useEffect(() => { + isVisibleRef.current = isVisible; + }, [isVisible]); + + useEffect(() => { + const checkAuth = () => { + const jwtToken = getCookie("jwt"); + const isAuth = !!(jwtToken && jwtToken.trim() !== ""); + + setIsAuthenticated((prev) => { + if (prev !== isAuth) { + return isAuth; + } + return prev; + }); + }; + + checkAuth(); + + const authCheckInterval = setInterval(checkAuth, 5000); + + return () => clearInterval(authCheckInterval); + }, []); + + function hardRefresh() { + try { + if ( + terminal && + typeof ( + terminal as { refresh?: (start: number, end: number) => void } + ).refresh === "function" + ) { + ( + terminal as { refresh?: (start: number, end: number) => void } + ).refresh(0, terminal.rows - 1); + } + } catch { + // Ignore terminal refresh errors + } + } + + function handleTotpSubmit(code: string) { + if (webSocketRef.current && code) { + webSocketRef.current.send( + JSON.stringify({ + type: "totp_response", + data: { code }, + }), + ); + setTotpRequired(false); + setTotpPrompt(""); + } + } + + function handleTotpCancel() { setTotpRequired(false); setTotpPrompt(""); + if (onClose) onClose(); } - } - function handleTotpCancel() { - setTotpRequired(false); - setTotpPrompt(""); - if (onClose) onClose(); - } - - function scheduleNotify(cols: number, rows: number) { - if (!(cols > 0 && rows > 0)) return; - pendingSizeRef.current = { cols, rows }; - if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current); - notifyTimerRef.current = setTimeout(() => { - const next = pendingSizeRef.current; - const last = lastSentSizeRef.current; - if (!next) return; - if (last && last.cols === next.cols && last.rows === next.rows) return; - if (webSocketRef.current?.readyState === WebSocket.OPEN) { - webSocketRef.current.send( - JSON.stringify({ type: "resize", data: next }), - ); - lastSentSizeRef.current = next; - } - }, DEBOUNCE_MS); - } - - useImperativeHandle( - ref, - () => ({ - disconnect: () => { - isUnmountingRef.current = true; - shouldNotReconnectRef.current = true; - isReconnectingRef.current = false; - if (pingIntervalRef.current) { - clearInterval(pingIntervalRef.current); - pingIntervalRef.current = null; + function scheduleNotify(cols: number, rows: number) { + if (!(cols > 0 && rows > 0)) return; + pendingSizeRef.current = { cols, rows }; + if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current); + notifyTimerRef.current = setTimeout(() => { + const next = pendingSizeRef.current; + const last = lastSentSizeRef.current; + if (!next) return; + if (last && last.cols === next.cols && last.rows === next.rows) return; + if (webSocketRef.current?.readyState === WebSocket.OPEN) { + webSocketRef.current.send( + JSON.stringify({ type: "resize", data: next }), + ); + lastSentSizeRef.current = next; } - if (reconnectTimeoutRef.current) { - clearTimeout(reconnectTimeoutRef.current); - reconnectTimeoutRef.current = null; - } - if (connectionTimeoutRef.current) { - clearTimeout(connectionTimeoutRef.current); - connectionTimeoutRef.current = null; - } - webSocketRef.current?.close(); - setIsConnected(false); - setIsConnecting(false); - }, - fit: () => { - fitAddonRef.current?.fit(); - if (terminal) scheduleNotify(terminal.cols, terminal.rows); - hardRefresh(); - }, - sendInput: (data: string) => { - if (webSocketRef.current?.readyState === 1) { - webSocketRef.current.send(JSON.stringify({ type: "input", data })); - } - }, - notifyResize: () => { - try { - const cols = terminal?.cols ?? undefined; - const rows = terminal?.rows ?? undefined; - if (typeof cols === "number" && typeof rows === "number") { - scheduleNotify(cols, rows); - hardRefresh(); + }, DEBOUNCE_MS); + } + + useImperativeHandle( + ref, + () => ({ + disconnect: () => { + isUnmountingRef.current = true; + shouldNotReconnectRef.current = true; + isReconnectingRef.current = false; + if (pingIntervalRef.current) { + clearInterval(pingIntervalRef.current); + pingIntervalRef.current = null; } - } catch { - // Ignore resize notification errors - } - }, - refresh: () => hardRefresh(), - }), - [terminal], - ); - - function handleWindowResize() { - if (!isVisibleRef.current) return; - fitAddonRef.current?.fit(); - if (terminal) scheduleNotify(terminal.cols, terminal.rows); - hardRefresh(); - } - - function getUseRightClickCopyPaste() { - return getCookie("rightClickCopyPaste") === "true"; - } - - function attemptReconnection() { - if ( - isUnmountingRef.current || - shouldNotReconnectRef.current || - isReconnectingRef.current || - isConnectingRef.current || - wasDisconnectedBySSH.current - ) { - return; - } - - if (reconnectAttempts.current >= maxReconnectAttempts) { - toast.error(t("terminal.maxReconnectAttemptsReached")); - if (onClose) { - onClose(); - } - return; - } - - isReconnectingRef.current = true; - - if (terminal) { - terminal.clear(); - } - - reconnectAttempts.current++; - - toast.info( - t("terminal.reconnecting", { - attempt: reconnectAttempts.current, - max: maxReconnectAttempts, - }), - ); - - reconnectTimeoutRef.current = setTimeout(() => { - if ( - isUnmountingRef.current || - shouldNotReconnectRef.current || - wasDisconnectedBySSH.current - ) { - isReconnectingRef.current = false; - return; - } - - if (reconnectAttempts.current > maxReconnectAttempts) { - isReconnectingRef.current = false; - return; - } - - const jwtToken = getCookie("jwt"); - if (!jwtToken || jwtToken.trim() === "") { - console.warn("Reconnection cancelled - no authentication token"); - isReconnectingRef.current = false; - setConnectionError("Authentication required for reconnection"); - return; - } - - if (terminal && hostConfig) { - terminal.clear(); - const cols = terminal.cols; - const rows = terminal.rows; - connectToHost(cols, rows); - } - - isReconnectingRef.current = false; - }, 2000 * reconnectAttempts.current); - } - - function connectToHost(cols: number, rows: number) { - if (isConnectingRef.current) { - return; - } - - isConnectingRef.current = true; - - const isDev = - process.env.NODE_ENV === "development" && - (window.location.port === "3000" || - window.location.port === "5173" || - window.location.port === ""); - - const jwtToken = getCookie("jwt"); - - if (!jwtToken || jwtToken.trim() === "") { - console.error("No JWT token available for WebSocket connection"); - setIsConnected(false); - setIsConnecting(false); - setConnectionError("Authentication required"); - isConnectingRef.current = false; - return; - } - - const baseWsUrl = isDev - ? `${window.location.protocol === "https:" ? "wss" : "ws"}://localhost:30002` - : isElectron() - ? (() => { - const baseUrl = - (window as any).configuredServerUrl || "http://127.0.0.1:30001"; - const wsProtocol = baseUrl.startsWith("https://") - ? "wss://" - : "ws://"; - const wsHost = baseUrl.replace(/^https?:\/\//, ""); - return `${wsProtocol}${wsHost}/ssh/websocket/`; - })() - : `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/ssh/websocket/`; - - if ( - webSocketRef.current && - webSocketRef.current.readyState !== WebSocket.CLOSED - ) { - webSocketRef.current.close(); - } - - if (pingIntervalRef.current) { - clearInterval(pingIntervalRef.current); - pingIntervalRef.current = null; - } - if (connectionTimeoutRef.current) { - clearTimeout(connectionTimeoutRef.current); - connectionTimeoutRef.current = null; - } - - const wsUrl = `${baseWsUrl}?token=${encodeURIComponent(jwtToken)}`; - - const ws = new WebSocket(wsUrl); - webSocketRef.current = ws; - wasDisconnectedBySSH.current = false; - setConnectionError(null); - shouldNotReconnectRef.current = false; - isReconnectingRef.current = false; - setIsConnecting(true); - - setupWebSocketListeners(ws, cols, rows); - } - - function setupWebSocketListeners(ws: WebSocket, cols: number, rows: number) { - ws.addEventListener("open", () => { - connectionTimeoutRef.current = setTimeout(() => { - if (!isConnected) { - if (terminal) { - terminal.clear(); + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current); + reconnectTimeoutRef.current = null; } - toast.error(t("terminal.connectionTimeout")); - if (webSocketRef.current) { - webSocketRef.current.close(); - } - if (reconnectAttempts.current > 0) { - attemptReconnection(); - } - } - }, 10000); - - ws.send( - JSON.stringify({ - type: "connectToHost", - data: { cols, rows, hostConfig, initialPath, executeCommand }, - }), - ); - terminal.onData((data) => { - ws.send(JSON.stringify({ type: "input", data })); - }); - - pingIntervalRef.current = setInterval(() => { - if (ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ type: "ping" })); - } - }, 30000); - }); - - ws.addEventListener("message", (event) => { - try { - const msg = JSON.parse(event.data); - if (msg.type === "data") { - if (typeof msg.data === "string") { - terminal.write(msg.data); - } else { - terminal.write(String(msg.data)); - } - } else if (msg.type === "error") { - const errorMessage = msg.message || t("terminal.unknownError"); - - if ( - errorMessage.toLowerCase().includes("auth") || - errorMessage.toLowerCase().includes("password") || - errorMessage.toLowerCase().includes("permission") || - errorMessage.toLowerCase().includes("denied") || - errorMessage.toLowerCase().includes("invalid") || - errorMessage.toLowerCase().includes("failed") || - errorMessage.toLowerCase().includes("incorrect") - ) { - toast.error(t("terminal.authError", { message: errorMessage })); - shouldNotReconnectRef.current = true; - if (webSocketRef.current) { - webSocketRef.current.close(); - } - if (onClose) { - onClose(); - } - return; - } - - if ( - errorMessage.toLowerCase().includes("connection") || - errorMessage.toLowerCase().includes("timeout") || - errorMessage.toLowerCase().includes("network") - ) { - toast.error( - t("terminal.connectionError", { message: errorMessage }), - ); - setIsConnected(false); - if (terminal) { - terminal.clear(); - } - setIsConnecting(true); - wasDisconnectedBySSH.current = false; - attemptReconnection(); - return; - } - - toast.error(t("terminal.error", { message: errorMessage })); - } else if (msg.type === "connected") { - setIsConnected(true); - setIsConnecting(false); - isConnectingRef.current = false; if (connectionTimeoutRef.current) { clearTimeout(connectionTimeoutRef.current); connectionTimeoutRef.current = null; } - if (reconnectAttempts.current > 0) { - toast.success(t("terminal.reconnected")); - } - reconnectAttempts.current = 0; - isReconnectingRef.current = false; - } else if (msg.type === "disconnected") { - wasDisconnectedBySSH.current = true; + webSocketRef.current?.close(); setIsConnected(false); - if (terminal) { - terminal.clear(); - } setIsConnecting(false); - if (onClose) { - onClose(); - } - } else if (msg.type === "totp_required") { - setTotpRequired(true); - setTotpPrompt(msg.prompt || "Verification code:"); - } - } catch (error) { - toast.error(t("terminal.messageParseError")); - } - }); - - ws.addEventListener("close", (event) => { - setIsConnected(false); - isConnectingRef.current = false; - if (terminal) { - terminal.clear(); - } - - if (event.code === 1008) { - console.error("WebSocket authentication failed:", event.reason); - setConnectionError("Authentication failed - please re-login"); - setIsConnecting(false); - shouldNotReconnectRef.current = true; - - localStorage.removeItem("jwt"); - - toast.error("Authentication failed. Please log in again."); - - return; - } - - setIsConnecting(false); - if ( - !wasDisconnectedBySSH.current && - !isUnmountingRef.current && - !shouldNotReconnectRef.current - ) { - wasDisconnectedBySSH.current = false; - attemptReconnection(); - } - }); - - ws.addEventListener("error", (event) => { - setIsConnected(false); - isConnectingRef.current = false; - setConnectionError(t("terminal.websocketError")); - if (terminal) { - terminal.clear(); - } - setIsConnecting(false); - if (!isUnmountingRef.current && !shouldNotReconnectRef.current) { - wasDisconnectedBySSH.current = false; - attemptReconnection(); - } - }); - } - - async function writeTextToClipboard(text: string): Promise { - try { - if (navigator.clipboard && navigator.clipboard.writeText) { - await navigator.clipboard.writeText(text); - return; - } - } catch { - // Clipboard API not available, fallback to textarea method - } - const textarea = document.createElement("textarea"); - textarea.value = text; - textarea.style.position = "fixed"; - textarea.style.left = "-9999px"; - document.body.appendChild(textarea); - textarea.focus(); - textarea.select(); - try { - document.execCommand("copy"); - } finally { - document.body.removeChild(textarea); - } - } - - async function readTextFromClipboard(): Promise { - try { - if (navigator.clipboard && navigator.clipboard.readText) { - return await navigator.clipboard.readText(); - } - } catch { - // Clipboard read not available or not permitted - } - return ""; - } - - useEffect(() => { - if (!terminal || !xtermRef.current) return; - - terminal.options = { - cursorBlink: true, - cursorStyle: "bar", - scrollback: 10000, - fontSize: 14, - fontFamily: - '"Caskaydia Cove Nerd Font Mono", "SF Mono", Consolas, "Liberation Mono", monospace', - theme: { background: "#18181b", foreground: "#f7f7f7" }, - allowTransparency: true, - convertEol: true, - windowsMode: false, - macOptionIsMeta: false, - macOptionClickForcesSelection: false, - rightClickSelectsWord: false, - fastScrollModifier: "alt", - fastScrollSensitivity: 5, - allowProposedApi: true, - minimumContrastRatio: 1, - letterSpacing: 0, - lineHeight: 1.2, - }; - - const fitAddon = new FitAddon(); - const clipboardAddon = new ClipboardAddon(); - const unicode11Addon = new Unicode11Addon(); - const webLinksAddon = new WebLinksAddon(); - - fitAddonRef.current = fitAddon; - terminal.loadAddon(fitAddon); - terminal.loadAddon(clipboardAddon); - terminal.loadAddon(unicode11Addon); - terminal.loadAddon(webLinksAddon); - - terminal.unicode.activeVersion = "11"; - - terminal.open(xtermRef.current); - - const element = xtermRef.current; - const handleContextMenu = async (e: MouseEvent) => { - if (!getUseRightClickCopyPaste()) return; - e.preventDefault(); - e.stopPropagation(); - try { - if (terminal.hasSelection()) { - const selection = terminal.getSelection(); - if (selection) { - await writeTextToClipboard(selection); - terminal.clearSelection(); - } - } else { - const pasteText = await readTextFromClipboard(); - if (pasteText) terminal.paste(pasteText); - } - } catch { - // Ignore clipboard operation errors - } - }; - element?.addEventListener("contextmenu", handleContextMenu); - - const handleMacKeyboard = (e: KeyboardEvent) => { - const isMacOS = - navigator.platform.toUpperCase().indexOf("MAC") >= 0 || - navigator.userAgent.toUpperCase().indexOf("MAC") >= 0; - - if (!isMacOS) return; - - if (e.altKey && !e.metaKey && !e.ctrlKey) { - const keyMappings: { [key: string]: string } = { - "7": "|", - "2": "€", - "8": "[", - "9": "]", - l: "@", - L: "@", - Digit7: "|", - Digit2: "€", - Digit8: "[", - Digit9: "]", - KeyL: "@", - }; - - const char = keyMappings[e.key] || keyMappings[e.code]; - if (char) { - e.preventDefault(); - e.stopPropagation(); - + }, + fit: () => { + fitAddonRef.current?.fit(); + if (terminal) scheduleNotify(terminal.cols, terminal.rows); + hardRefresh(); + }, + sendInput: (data: string) => { if (webSocketRef.current?.readyState === 1) { - webSocketRef.current.send( - JSON.stringify({ type: "input", data: char }), - ); + webSocketRef.current.send(JSON.stringify({ type: "input", data })); } - return false; - } + }, + notifyResize: () => { + try { + const cols = terminal?.cols ?? undefined; + const rows = terminal?.rows ?? undefined; + if (typeof cols === "number" && typeof rows === "number") { + scheduleNotify(cols, rows); + hardRefresh(); + } + } catch { + // Ignore resize notification errors + } + }, + refresh: () => hardRefresh(), + }), + [terminal], + ); + + function handleWindowResize() { + if (!isVisibleRef.current) return; + fitAddonRef.current?.fit(); + if (terminal) scheduleNotify(terminal.cols, terminal.rows); + hardRefresh(); + } + + function getUseRightClickCopyPaste() { + return getCookie("rightClickCopyPaste") === "true"; + } + + function attemptReconnection() { + if ( + isUnmountingRef.current || + shouldNotReconnectRef.current || + isReconnectingRef.current || + isConnectingRef.current || + wasDisconnectedBySSH.current + ) { + return; } - }; - element?.addEventListener("keydown", handleMacKeyboard, true); + if (reconnectAttempts.current >= maxReconnectAttempts) { + toast.error(t("terminal.maxReconnectAttemptsReached")); + if (onClose) { + onClose(); + } + return; + } - const resizeObserver = new ResizeObserver(() => { - if (resizeTimeout.current) clearTimeout(resizeTimeout.current); - resizeTimeout.current = setTimeout(() => { - if (!isVisibleRef.current) return; - fitAddonRef.current?.fit(); - if (terminal) scheduleNotify(terminal.cols, terminal.rows); - hardRefresh(); - }, 150); - }); + isReconnectingRef.current = true; - resizeObserver.observe(xtermRef.current); + if (terminal) { + terminal.clear(); + } - setVisible(true); + reconnectAttempts.current++; + + toast.info( + t("terminal.reconnecting", { + attempt: reconnectAttempts.current, + max: maxReconnectAttempts, + }), + ); + + reconnectTimeoutRef.current = setTimeout(() => { + if ( + isUnmountingRef.current || + shouldNotReconnectRef.current || + wasDisconnectedBySSH.current + ) { + isReconnectingRef.current = false; + return; + } + + if (reconnectAttempts.current > maxReconnectAttempts) { + isReconnectingRef.current = false; + return; + } + + const jwtToken = getCookie("jwt"); + if (!jwtToken || jwtToken.trim() === "") { + console.warn("Reconnection cancelled - no authentication token"); + isReconnectingRef.current = false; + setConnectionError("Authentication required for reconnection"); + return; + } + + if (terminal && hostConfig) { + terminal.clear(); + const cols = terminal.cols; + const rows = terminal.rows; + connectToHost(cols, rows); + } + + isReconnectingRef.current = false; + }, 2000 * reconnectAttempts.current); + } + + function connectToHost(cols: number, rows: number) { + if (isConnectingRef.current) { + return; + } + + isConnectingRef.current = true; + + const isDev = + process.env.NODE_ENV === "development" && + (window.location.port === "3000" || + window.location.port === "5173" || + window.location.port === ""); + + const jwtToken = getCookie("jwt"); + + if (!jwtToken || jwtToken.trim() === "") { + console.error("No JWT token available for WebSocket connection"); + setIsConnected(false); + setIsConnecting(false); + setConnectionError("Authentication required"); + isConnectingRef.current = false; + return; + } + + const baseWsUrl = isDev + ? `${window.location.protocol === "https:" ? "wss" : "ws"}://localhost:30002` + : isElectron() + ? (() => { + const baseUrl = + (window as { configuredServerUrl?: string }) + .configuredServerUrl || "http://127.0.0.1:30001"; + const wsProtocol = baseUrl.startsWith("https://") + ? "wss://" + : "ws://"; + const wsHost = baseUrl.replace(/^https?:\/\//, ""); + return `${wsProtocol}${wsHost}/ssh/websocket/`; + })() + : `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/ssh/websocket/`; + + if ( + webSocketRef.current && + webSocketRef.current.readyState !== WebSocket.CLOSED + ) { + webSocketRef.current.close(); + } - return () => { - isUnmountingRef.current = true; - shouldNotReconnectRef.current = true; - isReconnectingRef.current = false; - setIsConnecting(false); - resizeObserver.disconnect(); - element?.removeEventListener("contextmenu", handleContextMenu); - element?.removeEventListener("keydown", handleMacKeyboard, true); - if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current); - if (resizeTimeout.current) clearTimeout(resizeTimeout.current); - if (reconnectTimeoutRef.current) - clearTimeout(reconnectTimeoutRef.current); - if (connectionTimeoutRef.current) - clearTimeout(connectionTimeoutRef.current); if (pingIntervalRef.current) { clearInterval(pingIntervalRef.current); pingIntervalRef.current = null; } - webSocketRef.current?.close(); - }; - }, [xtermRef, terminal]); + if (connectionTimeoutRef.current) { + clearTimeout(connectionTimeoutRef.current); + connectionTimeoutRef.current = null; + } - useEffect(() => { - if (!terminal || !hostConfig || !visible) return; + const wsUrl = `${baseWsUrl}?token=${encodeURIComponent(jwtToken)}`; - if (isConnected || isConnecting) return; + const ws = new WebSocket(wsUrl); + webSocketRef.current = ws; + wasDisconnectedBySSH.current = false; + setConnectionError(null); + shouldNotReconnectRef.current = false; + isReconnectingRef.current = false; + setIsConnecting(true); - setIsConnecting(true); + setupWebSocketListeners(ws, cols, rows); + } - const readyFonts = - (document as any).fonts?.ready instanceof Promise - ? (document as any).fonts.ready - : Promise.resolve(); + function setupWebSocketListeners( + ws: WebSocket, + cols: number, + rows: number, + ) { + ws.addEventListener("open", () => { + connectionTimeoutRef.current = setTimeout(() => { + if (!isConnected) { + if (terminal) { + terminal.clear(); + } + toast.error(t("terminal.connectionTimeout")); + if (webSocketRef.current) { + webSocketRef.current.close(); + } + if (reconnectAttempts.current > 0) { + attemptReconnection(); + } + } + }, 10000); - readyFonts.then(() => { - setTimeout(() => { - fitAddonRef.current?.fit(); - if (terminal) scheduleNotify(terminal.cols, terminal.rows); - hardRefresh(); + ws.send( + JSON.stringify({ + type: "connectToHost", + data: { cols, rows, hostConfig, initialPath, executeCommand }, + }), + ); + terminal.onData((data) => { + ws.send(JSON.stringify({ type: "input", data })); + }); - if (terminal && !splitScreen) { - terminal.focus(); + pingIntervalRef.current = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: "ping" })); + } + }, 30000); + }); + + ws.addEventListener("message", (event) => { + try { + const msg = JSON.parse(event.data); + if (msg.type === "data") { + if (typeof msg.data === "string") { + terminal.write(msg.data); + } else { + terminal.write(String(msg.data)); + } + } else if (msg.type === "error") { + const errorMessage = msg.message || t("terminal.unknownError"); + + if ( + errorMessage.toLowerCase().includes("auth") || + errorMessage.toLowerCase().includes("password") || + errorMessage.toLowerCase().includes("permission") || + errorMessage.toLowerCase().includes("denied") || + errorMessage.toLowerCase().includes("invalid") || + errorMessage.toLowerCase().includes("failed") || + errorMessage.toLowerCase().includes("incorrect") + ) { + toast.error(t("terminal.authError", { message: errorMessage })); + shouldNotReconnectRef.current = true; + if (webSocketRef.current) { + webSocketRef.current.close(); + } + if (onClose) { + onClose(); + } + return; + } + + if ( + errorMessage.toLowerCase().includes("connection") || + errorMessage.toLowerCase().includes("timeout") || + errorMessage.toLowerCase().includes("network") + ) { + toast.error( + t("terminal.connectionError", { message: errorMessage }), + ); + setIsConnected(false); + if (terminal) { + terminal.clear(); + } + setIsConnecting(true); + wasDisconnectedBySSH.current = false; + attemptReconnection(); + return; + } + + toast.error(t("terminal.error", { message: errorMessage })); + } else if (msg.type === "connected") { + setIsConnected(true); + setIsConnecting(false); + isConnectingRef.current = false; + if (connectionTimeoutRef.current) { + clearTimeout(connectionTimeoutRef.current); + connectionTimeoutRef.current = null; + } + if (reconnectAttempts.current > 0) { + toast.success(t("terminal.reconnected")); + } + reconnectAttempts.current = 0; + isReconnectingRef.current = false; + } else if (msg.type === "disconnected") { + wasDisconnectedBySSH.current = true; + setIsConnected(false); + if (terminal) { + terminal.clear(); + } + setIsConnecting(false); + if (onClose) { + onClose(); + } + } else if (msg.type === "totp_required") { + setTotpRequired(true); + setTotpPrompt(msg.prompt || "Verification code:"); + } + } catch (error) { + toast.error(t("terminal.messageParseError")); + } + }); + + ws.addEventListener("close", (event) => { + setIsConnected(false); + isConnectingRef.current = false; + if (terminal) { + terminal.clear(); } - const jwtToken = getCookie("jwt"); - - if (!jwtToken || jwtToken.trim() === "") { - setIsConnected(false); + if (event.code === 1008) { + console.error("WebSocket authentication failed:", event.reason); + setConnectionError("Authentication failed - please re-login"); setIsConnecting(false); - setConnectionError("Authentication required"); + shouldNotReconnectRef.current = true; + + localStorage.removeItem("jwt"); + + toast.error("Authentication failed. Please log in again."); + return; } - const cols = terminal.cols; - const rows = terminal.rows; + setIsConnecting(false); + if ( + !wasDisconnectedBySSH.current && + !isUnmountingRef.current && + !shouldNotReconnectRef.current + ) { + wasDisconnectedBySSH.current = false; + attemptReconnection(); + } + }); - connectToHost(cols, rows); - }, 200); - }); - }, [terminal, hostConfig, visible, isConnected, isConnecting, splitScreen]); + ws.addEventListener("error", (event) => { + setIsConnected(false); + isConnectingRef.current = false; + setConnectionError(t("terminal.websocketError")); + if (terminal) { + terminal.clear(); + } + setIsConnecting(false); + if (!isUnmountingRef.current && !shouldNotReconnectRef.current) { + wasDisconnectedBySSH.current = false; + attemptReconnection(); + } + }); + } - useEffect(() => { - if (isVisible && fitAddonRef.current) { + async function writeTextToClipboard(text: string): Promise { + try { + if (navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(text); + return; + } + } catch { + // Clipboard API not available, fallback to textarea method + } + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.style.position = "fixed"; + textarea.style.left = "-9999px"; + document.body.appendChild(textarea); + textarea.focus(); + textarea.select(); + try { + document.execCommand("copy"); + } finally { + document.body.removeChild(textarea); + } + } + + async function readTextFromClipboard(): Promise { + try { + if (navigator.clipboard && navigator.clipboard.readText) { + return await navigator.clipboard.readText(); + } + } catch { + // Clipboard read not available or not permitted + } + return ""; + } + + useEffect(() => { + if (!terminal || !xtermRef.current) return; + + terminal.options = { + cursorBlink: true, + cursorStyle: "bar", + scrollback: 10000, + fontSize: 14, + fontFamily: + '"Caskaydia Cove Nerd Font Mono", "SF Mono", Consolas, "Liberation Mono", monospace', + theme: { background: "#18181b", foreground: "#f7f7f7" }, + allowTransparency: true, + convertEol: true, + windowsMode: false, + macOptionIsMeta: false, + macOptionClickForcesSelection: false, + rightClickSelectsWord: false, + fastScrollModifier: "alt", + fastScrollSensitivity: 5, + allowProposedApi: true, + minimumContrastRatio: 1, + letterSpacing: 0, + lineHeight: 1.2, + }; + + const fitAddon = new FitAddon(); + const clipboardAddon = new ClipboardAddon(); + const unicode11Addon = new Unicode11Addon(); + const webLinksAddon = new WebLinksAddon(); + + fitAddonRef.current = fitAddon; + terminal.loadAddon(fitAddon); + terminal.loadAddon(clipboardAddon); + terminal.loadAddon(unicode11Addon); + terminal.loadAddon(webLinksAddon); + + terminal.unicode.activeVersion = "11"; + + terminal.open(xtermRef.current); + + const element = xtermRef.current; + const handleContextMenu = async (e: MouseEvent) => { + if (!getUseRightClickCopyPaste()) return; + e.preventDefault(); + e.stopPropagation(); + try { + if (terminal.hasSelection()) { + const selection = terminal.getSelection(); + if (selection) { + await writeTextToClipboard(selection); + terminal.clearSelection(); + } + } else { + const pasteText = await readTextFromClipboard(); + if (pasteText) terminal.paste(pasteText); + } + } catch { + // Ignore clipboard operation errors + } + }; + element?.addEventListener("contextmenu", handleContextMenu); + + const handleMacKeyboard = (e: KeyboardEvent) => { + const isMacOS = + navigator.platform.toUpperCase().indexOf("MAC") >= 0 || + navigator.userAgent.toUpperCase().indexOf("MAC") >= 0; + + if (!isMacOS) return; + + if (e.altKey && !e.metaKey && !e.ctrlKey) { + const keyMappings: { [key: string]: string } = { + "7": "|", + "2": "€", + "8": "[", + "9": "]", + l: "@", + L: "@", + Digit7: "|", + Digit2: "€", + Digit8: "[", + Digit9: "]", + KeyL: "@", + }; + + const char = keyMappings[e.key] || keyMappings[e.code]; + if (char) { + e.preventDefault(); + e.stopPropagation(); + + if (webSocketRef.current?.readyState === 1) { + webSocketRef.current.send( + JSON.stringify({ type: "input", data: char }), + ); + } + return false; + } + } + }; + + element?.addEventListener("keydown", handleMacKeyboard, true); + + const resizeObserver = new ResizeObserver(() => { + if (resizeTimeout.current) clearTimeout(resizeTimeout.current); + resizeTimeout.current = setTimeout(() => { + if (!isVisibleRef.current) return; + fitAddonRef.current?.fit(); + if (terminal) scheduleNotify(terminal.cols, terminal.rows); + hardRefresh(); + }, 150); + }); + + resizeObserver.observe(xtermRef.current); + + setVisible(true); + + return () => { + isUnmountingRef.current = true; + shouldNotReconnectRef.current = true; + isReconnectingRef.current = false; + setIsConnecting(false); + resizeObserver.disconnect(); + element?.removeEventListener("contextmenu", handleContextMenu); + element?.removeEventListener("keydown", handleMacKeyboard, true); + if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current); + if (resizeTimeout.current) clearTimeout(resizeTimeout.current); + if (reconnectTimeoutRef.current) + clearTimeout(reconnectTimeoutRef.current); + if (connectionTimeoutRef.current) + clearTimeout(connectionTimeoutRef.current); + if (pingIntervalRef.current) { + clearInterval(pingIntervalRef.current); + pingIntervalRef.current = null; + } + webSocketRef.current?.close(); + }; + }, [xtermRef, terminal]); + + useEffect(() => { + if (!terminal || !hostConfig || !visible) return; + + if (isConnected || isConnecting) return; + + setIsConnecting(true); + + const readyFonts = + (document as { fonts?: { ready?: Promise } }).fonts + ?.ready instanceof Promise + ? (document as { fonts?: { ready?: Promise } }).fonts.ready + : Promise.resolve(); + + readyFonts.then(() => { + setTimeout(() => { + fitAddonRef.current?.fit(); + if (terminal) scheduleNotify(terminal.cols, terminal.rows); + hardRefresh(); + + if (terminal && !splitScreen) { + terminal.focus(); + } + + const jwtToken = getCookie("jwt"); + + if (!jwtToken || jwtToken.trim() === "") { + setIsConnected(false); + setIsConnecting(false); + setConnectionError("Authentication required"); + return; + } + + const cols = terminal.cols; + const rows = terminal.rows; + + connectToHost(cols, rows); + }, 200); + }); + }, [terminal, hostConfig, visible, isConnected, isConnecting, splitScreen]); + + useEffect(() => { + if (isVisible && fitAddonRef.current) { + setTimeout(() => { + fitAddonRef.current?.fit(); + if (terminal) scheduleNotify(terminal.cols, terminal.rows); + hardRefresh(); + if (terminal && !splitScreen) { + terminal.focus(); + } + }, 0); + + if (terminal && !splitScreen) { + setTimeout(() => { + terminal.focus(); + }, 100); + } + } + }, [isVisible, splitScreen, terminal]); + + useEffect(() => { + if (!fitAddonRef.current) return; setTimeout(() => { fitAddonRef.current?.fit(); if (terminal) scheduleNotify(terminal.cols, terminal.rows); hardRefresh(); - if (terminal && !splitScreen) { + if (terminal && !splitScreen && isVisible) { terminal.focus(); } }, 0); + }, [splitScreen, isVisible, terminal]); - if (terminal && !splitScreen) { - setTimeout(() => { - terminal.focus(); - }, 100); - } - } - }, [isVisible, splitScreen, terminal]); + return ( +
+
{ + if (terminal && !splitScreen) { + terminal.focus(); + } + }} + /> - useEffect(() => { - if (!fitAddonRef.current) return; - setTimeout(() => { - fitAddonRef.current?.fit(); - if (terminal) scheduleNotify(terminal.cols, terminal.rows); - hardRefresh(); - if (terminal && !splitScreen && isVisible) { - terminal.focus(); - } - }, 0); - }, [splitScreen, isVisible, terminal]); - - return ( -
-
{ - if (terminal && !splitScreen) { - terminal.focus(); - } - }} - /> - - {isConnecting && ( -
-
-
- {t("terminal.connecting")} + {isConnecting && ( +
+
+
+ {t("terminal.connecting")} +
-
- )} + )} - -
- ); -}); + +
+ ); + }, +); const style = document.createElement("style"); style.innerHTML = ` diff --git a/src/ui/Desktop/Navigation/AppView.tsx b/src/ui/Desktop/Navigation/AppView.tsx index 11074cac..a8e1bb2e 100644 --- a/src/ui/Desktop/Navigation/AppView.tsx +++ b/src/ui/Desktop/Navigation/AppView.tsx @@ -18,6 +18,21 @@ import { } from "lucide-react"; import { Button } from "@/components/ui/button.tsx"; +interface TabData { + id: number; + type: string; + title: string; + terminalRef?: { + current?: { + fit?: () => void; + notifyResize?: () => void; + refresh?: () => void; + }; + }; + hostConfig?: unknown; + [key: string]: unknown; +} + interface TerminalViewProps { isTopbarOpen?: boolean; } @@ -25,11 +40,16 @@ interface TerminalViewProps { export function AppView({ isTopbarOpen = true, }: TerminalViewProps): React.ReactElement { - const { tabs, currentTab, allSplitScreenTab, removeTab } = useTabs() as any; + const { tabs, currentTab, allSplitScreenTab, removeTab } = useTabs() as { + tabs: TabData[]; + currentTab: number; + allSplitScreenTab: number[]; + removeTab: (id: number) => void; + }; const { state: sidebarState } = useSidebar(); const terminalTabs = tabs.filter( - (tab: any) => + (tab: TabData) => tab.type === "terminal" || tab.type === "server" || tab.type === "file_manager", @@ -59,7 +79,7 @@ export function AppView({ const splitIds = allSplitScreenTab as number[]; visibleIds.push(currentTab, ...splitIds.filter((i) => i !== currentTab)); } - terminalTabs.forEach((t: any) => { + terminalTabs.forEach((t: TabData) => { if (visibleIds.includes(t.id)) { const ref = t.terminalRef?.current; if (ref?.fit) ref.fit(); @@ -125,16 +145,16 @@ export function AppView({ const renderTerminalsLayer = () => { const styles: Record = {}; - const splitTabs = terminalTabs.filter((tab: any) => + const splitTabs = terminalTabs.filter((tab: TabData) => allSplitScreenTab.includes(tab.id), ); - const mainTab = terminalTabs.find((tab: any) => tab.id === currentTab); + const mainTab = terminalTabs.find((tab: TabData) => tab.id === currentTab); const layoutTabs = [ mainTab, ...splitTabs.filter( - (t: any) => t && t.id !== (mainTab && (mainTab as any).id), + (t: TabData) => t && t.id !== (mainTab && (mainTab as TabData).id), ), - ].filter(Boolean) as any[]; + ].filter((t): t is TabData => t !== null && t !== undefined); if (allSplitScreenTab.length === 0 && mainTab) { const isFileManagerTab = mainTab.type === "file_manager"; @@ -150,7 +170,7 @@ export function AppView({ opacity: ready ? 1 : 0, }; } else { - layoutTabs.forEach((t: any) => { + layoutTabs.forEach((t: TabData) => { const rect = panelRects[String(t.id)]; const parentRect = containerRef.current?.getBoundingClientRect(); if (rect && parentRect) { @@ -171,7 +191,7 @@ export function AppView({ return (
- {terminalTabs.map((t: any) => { + {terminalTabs.map((t: TabData) => { const hasStyle = !!styles[t.id]; const isVisible = hasStyle || (allSplitScreenTab.length === 0 && t.id === currentTab); @@ -241,16 +261,16 @@ export function AppView({ }; const renderSplitOverlays = () => { - const splitTabs = terminalTabs.filter((tab: any) => + const splitTabs = terminalTabs.filter((tab: TabData) => allSplitScreenTab.includes(tab.id), ); - const mainTab = terminalTabs.find((tab: any) => tab.id === currentTab); + const mainTab = terminalTabs.find((tab: TabData) => tab.id === currentTab); const layoutTabs = [ mainTab, ...splitTabs.filter( - (t: any) => t && t.id !== (mainTab && (mainTab as any).id), + (t: TabData) => t && t.id !== (mainTab && (mainTab as TabData).id), ), - ].filter(Boolean) as any[]; + ].filter((t): t is TabData => t !== null && t !== undefined); if (allSplitScreenTab.length === 0) return null; const handleStyle = { @@ -258,13 +278,16 @@ export function AppView({ zIndex: 12, background: "var(--color-dark-border)", } as React.CSSProperties; - const commonGroupProps = { + const commonGroupProps: { + onLayout: () => void; + onResize: () => void; + } = { onLayout: scheduleMeasureAndFit, onResize: scheduleMeasureAndFit, - } as any; + }; if (layoutTabs.length === 2) { - const [a, b] = layoutTabs as any[]; + const [a, b] = layoutTabs; return (
tab.id === currentTab); + const currentTabData = tabs.find((tab: TabData) => tab.id === currentTab); const isFileManager = currentTabData?.type === "file_manager"; const isSplitScreen = allSplitScreenTab.length > 0; diff --git a/src/ui/Desktop/Navigation/LeftSidebar.tsx b/src/ui/Desktop/Navigation/LeftSidebar.tsx index 72ae3bef..c51dbe29 100644 --- a/src/ui/Desktop/Navigation/LeftSidebar.tsx +++ b/src/ui/Desktop/Navigation/LeftSidebar.tsx @@ -57,7 +57,7 @@ interface SSHHost { enableTunnel: boolean; enableFileManager: boolean; defaultPath: string; - tunnelConnections: any[]; + tunnelConnections: unknown[]; createdAt: string; updatedAt: string; } @@ -112,13 +112,19 @@ export function LeftSidebar({ setCurrentTab, allSplitScreenTab, updateHostConfig, - } = useTabs() as any; + } = useTabs() as { + tabs: Array<{ id: number; type: string; [key: string]: unknown }>; + addTab: (tab: { type: string; [key: string]: unknown }) => number; + setCurrentTab: (id: number) => void; + allSplitScreenTab: number[]; + updateHostConfig: (id: number, config: unknown) => void; + }; const isSplitScreenActive = Array.isArray(allSplitScreenTab) && allSplitScreenTab.length > 0; const sshManagerTab = tabList.find((t) => t.type === "ssh_manager"); const openSshManagerTab = () => { if (sshManagerTab || isSplitScreenActive) return; - const id = addTab({ type: "ssh_manager" } as any); + const id = addTab({ type: "ssh_manager" }); setCurrentTab(id); }; const adminTab = tabList.find((t) => t.type === "admin"); @@ -128,7 +134,7 @@ export function LeftSidebar({ setCurrentTab(adminTab.id); return; } - const id = addTab({ type: "admin" } as any); + const id = addTab({ type: "admin" }); setCurrentTab(id); }; const userProfileTab = tabList.find((t) => t.type === "user_profile"); @@ -138,7 +144,7 @@ export function LeftSidebar({ setCurrentTab(userProfileTab.id); return; } - const id = addTab({ type: "user_profile" } as any); + const id = addTab({ type: "user_profile" }); setCurrentTab(id); }; @@ -206,7 +212,7 @@ export function LeftSidebar({ }); }, 50); } - } catch (err: any) { + } catch (err: unknown) { setHostsError(t("leftSidebar.failedToLoadHosts")); } }, [updateHostConfig]); @@ -319,9 +325,10 @@ export function LeftSidebar({ await deleteAccount(deletePassword); handleLogout(); - } catch (err: any) { + } catch (err: unknown) { setDeleteError( - err?.response?.data?.error || t("leftSidebar.failedToDeleteAccount"), + (err as { response?: { data?: { error?: string } } })?.response?.data + ?.error || t("leftSidebar.failedToDeleteAccount"), ); setDeleteLoading(false); } diff --git a/src/ui/Desktop/Navigation/TopNavbar.tsx b/src/ui/Desktop/Navigation/TopNavbar.tsx index 10487a1f..a5bc21d0 100644 --- a/src/ui/Desktop/Navigation/TopNavbar.tsx +++ b/src/ui/Desktop/Navigation/TopNavbar.tsx @@ -18,6 +18,18 @@ import { TabDropdown } from "@/ui/Desktop/Navigation/Tabs/TabDropdown.tsx"; import { getCookie, setCookie } from "@/ui/main-axios.ts"; import { SnippetsSidebar } from "@/ui/Desktop/Apps/Terminal/SnippetsSidebar.tsx"; +interface TabData { + id: number; + type: string; + title: string; + terminalRef?: { + current?: { + sendInput?: (data: string) => void; + }; + }; + [key: string]: unknown; +} + interface TopNavbarProps { isTopbarOpen: boolean; setIsTopbarOpen: (open: boolean) => void; @@ -35,7 +47,14 @@ export function TopNavbar({ setSplitScreenTab, removeTab, allSplitScreenTab, - } = useTabs() as any; + } = useTabs() as { + tabs: TabData[]; + currentTab: number; + setCurrentTab: (id: number) => void; + setSplitScreenTab: (id: number) => void; + removeTab: (id: number) => void; + allSplitScreenTab: number[]; + }; const leftPosition = state === "collapsed" ? "26px" : "264px"; const { t } = useTranslation(); @@ -192,7 +211,7 @@ export function TopNavbar({ if (commandToSend) { selectedTabIds.forEach((tabId) => { - const tab = tabs.find((t: any) => t.id === tabId); + const tab = tabs.find((t: TabData) => t.id === tabId); if (tab?.terminalRef?.current?.sendInput) { tab.terminalRef.current.sendInput(commandToSend); } @@ -206,7 +225,7 @@ export function TopNavbar({ if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) { const char = e.key; selectedTabIds.forEach((tabId) => { - const tab = tabs.find((t: any) => t.id === tabId); + const tab = tabs.find((t: TabData) => t.id === tabId); if (tab?.terminalRef?.current?.sendInput) { tab.terminalRef.current.sendInput(char); } @@ -215,7 +234,7 @@ export function TopNavbar({ }; const handleSnippetExecute = (content: string) => { - const tab = tabs.find((t: any) => t.id === currentTab); + const tab = tabs.find((t: TabData) => t.id === currentTab); if (tab?.terminalRef?.current?.sendInput) { tab.terminalRef.current.sendInput(content + "\n"); } @@ -223,13 +242,13 @@ export function TopNavbar({ const isSplitScreenActive = Array.isArray(allSplitScreenTab) && allSplitScreenTab.length > 0; - const currentTabObj = tabs.find((t: any) => t.id === currentTab); + const currentTabObj = tabs.find((t: TabData) => t.id === currentTab); const currentTabIsHome = currentTabObj?.type === "home"; const currentTabIsSshManager = currentTabObj?.type === "ssh_manager"; const currentTabIsAdmin = currentTabObj?.type === "admin"; const currentTabIsUserProfile = currentTabObj?.type === "user_profile"; - const terminalTabs = tabs.filter((tab: any) => tab.type === "terminal"); + const terminalTabs = tabs.filter((tab: TabData) => tab.type === "terminal"); const updateRightClickCopyPaste = (checked: boolean) => { setCookie("rightClickCopyPaste", checked.toString()); @@ -246,7 +265,7 @@ export function TopNavbar({ }} >
- {tabs.map((tab: any) => { + {tabs.map((tab: TabData) => { const isActive = tab.id === currentTab; const isSplit = Array.isArray(allSplitScreenTab) && diff --git a/src/ui/Mobile/Apps/Terminal/Terminal.tsx b/src/ui/Mobile/Apps/Terminal/Terminal.tsx index e27453e1..3cfbb362 100644 --- a/src/ui/Mobile/Apps/Terminal/Terminal.tsx +++ b/src/ui/Mobile/Apps/Terminal/Terminal.tsx @@ -14,379 +14,412 @@ import { useTranslation } from "react-i18next"; import { isElectron, getCookie } from "@/ui/main-axios.ts"; import { toast } from "sonner"; +interface HostConfig { + id?: number; + ip: string; + port: number; + username: string; + password?: string; + key?: string; + keyPassword?: string; + keyType?: string; + authType?: string; + credentialId?: number; + [key: string]: unknown; +} + +interface TerminalHandle { + disconnect: () => void; + fit: () => void; + sendInput: (data: string) => void; + notifyResize: () => void; + refresh: () => void; +} + interface SSHTerminalProps { - hostConfig: any; + hostConfig: HostConfig; isVisible: boolean; title?: string; } -export const Terminal = forwardRef(function SSHTerminal( - { hostConfig, isVisible }, - ref, -) { - const { t } = useTranslation(); - const { instance: terminal, ref: xtermRef } = useXTerm(); - const fitAddonRef = useRef(null); - const webSocketRef = useRef(null); - const resizeTimeout = useRef(null); - const wasDisconnectedBySSH = useRef(false); - const pingIntervalRef = useRef(null); - const [visible, setVisible] = useState(false); - const [isConnected, setIsConnected] = useState(false); - const [isConnecting, setIsConnecting] = useState(false); - const [connectionError, setConnectionError] = useState(null); - const [isAuthenticated, setIsAuthenticated] = useState(false); - const isVisibleRef = useRef(false); - const isConnectingRef = useRef(false); +export const Terminal = forwardRef( + function SSHTerminal({ hostConfig, isVisible }, ref) { + const { t } = useTranslation(); + const { instance: terminal, ref: xtermRef } = useXTerm(); + const fitAddonRef = useRef(null); + const webSocketRef = useRef(null); + const resizeTimeout = useRef(null); + const wasDisconnectedBySSH = useRef(false); + const pingIntervalRef = useRef(null); + const [visible, setVisible] = useState(false); + const [isConnected, setIsConnected] = useState(false); + const [isConnecting, setIsConnecting] = useState(false); + const [connectionError, setConnectionError] = useState(null); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const isVisibleRef = useRef(false); + const isConnectingRef = useRef(false); - const lastSentSizeRef = useRef<{ cols: number; rows: number } | null>(null); - const pendingSizeRef = useRef<{ cols: number; rows: number } | null>(null); - const notifyTimerRef = useRef(null); - const DEBOUNCE_MS = 140; + const lastSentSizeRef = useRef<{ cols: number; rows: number } | null>(null); + const pendingSizeRef = useRef<{ cols: number; rows: number } | null>(null); + const notifyTimerRef = useRef(null); + const DEBOUNCE_MS = 140; - useEffect(() => { - isVisibleRef.current = isVisible; - }, [isVisible]); + useEffect(() => { + isVisibleRef.current = isVisible; + }, [isVisible]); - useEffect(() => { - const checkAuth = () => { - const jwtToken = getCookie("jwt"); - const isAuth = !!(jwtToken && jwtToken.trim() !== ""); + useEffect(() => { + const checkAuth = () => { + const jwtToken = getCookie("jwt"); + const isAuth = !!(jwtToken && jwtToken.trim() !== ""); - setIsAuthenticated((prev) => { - if (prev !== isAuth) { - return isAuth; + setIsAuthenticated((prev) => { + if (prev !== isAuth) { + return isAuth; + } + return prev; + }); + }; + + checkAuth(); + + const authCheckInterval = setInterval(checkAuth, 5000); + + return () => clearInterval(authCheckInterval); + }, []); + + function hardRefresh() { + try { + if ( + terminal && + typeof ( + terminal as { refresh?: (start: number, end: number) => void } + ).refresh === "function" + ) { + ( + terminal as { refresh?: (start: number, end: number) => void } + ).refresh(0, terminal.rows - 1); } - return prev; - }); - }; - - checkAuth(); - - const authCheckInterval = setInterval(checkAuth, 5000); - - return () => clearInterval(authCheckInterval); - }, []); - - function hardRefresh() { - try { - if (terminal && typeof (terminal as any).refresh === "function") { - (terminal as any).refresh(0, terminal.rows - 1); + } catch { + // Ignore terminal refresh errors } - } catch { - // Ignore terminal refresh errors } - } - function scheduleNotify(cols: number, rows: number) { - if (!(cols > 0 && rows > 0)) return; - pendingSizeRef.current = { cols, rows }; - if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current); - notifyTimerRef.current = setTimeout(() => { - const next = pendingSizeRef.current; - const last = lastSentSizeRef.current; - if (!next) return; - if (last && last.cols === next.cols && last.rows === next.rows) return; - if (webSocketRef.current?.readyState === WebSocket.OPEN) { - webSocketRef.current.send( - JSON.stringify({ type: "resize", data: next }), + function scheduleNotify(cols: number, rows: number) { + if (!(cols > 0 && rows > 0)) return; + pendingSizeRef.current = { cols, rows }; + if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current); + notifyTimerRef.current = setTimeout(() => { + const next = pendingSizeRef.current; + const last = lastSentSizeRef.current; + if (!next) return; + if (last && last.cols === next.cols && last.rows === next.rows) return; + if (webSocketRef.current?.readyState === WebSocket.OPEN) { + webSocketRef.current.send( + JSON.stringify({ type: "resize", data: next }), + ); + lastSentSizeRef.current = next; + } + }, DEBOUNCE_MS); + } + + useImperativeHandle( + ref, + () => ({ + disconnect: () => { + if (pingIntervalRef.current) { + clearInterval(pingIntervalRef.current); + pingIntervalRef.current = null; + } + webSocketRef.current?.close(); + }, + fit: () => { + fitAddonRef.current?.fit(); + if (terminal) scheduleNotify(terminal.cols, terminal.rows); + hardRefresh(); + }, + sendInput: (data: string) => { + if (webSocketRef.current?.readyState === 1) { + webSocketRef.current.send(JSON.stringify({ type: "input", data })); + } + }, + notifyResize: () => { + try { + const cols = terminal?.cols ?? undefined; + const rows = terminal?.rows ?? undefined; + if (typeof cols === "number" && typeof rows === "number") { + scheduleNotify(cols, rows); + hardRefresh(); + } + } catch { + // Ignore resize notification errors + } + }, + refresh: () => hardRefresh(), + }), + [terminal], + ); + + function handleWindowResize() { + if (!isVisibleRef.current) return; + fitAddonRef.current?.fit(); + if (terminal) scheduleNotify(terminal.cols, terminal.rows); + hardRefresh(); + } + + function setupWebSocketListeners( + ws: WebSocket, + cols: number, + rows: number, + ) { + ws.addEventListener("open", () => { + ws.send( + JSON.stringify({ + type: "connectToHost", + data: { cols, rows, hostConfig }, + }), ); - lastSentSizeRef.current = next; - } - }, DEBOUNCE_MS); - } + terminal.onData((data) => { + ws.send(JSON.stringify({ type: "input", data })); + }); - useImperativeHandle( - ref, - () => ({ - disconnect: () => { + pingIntervalRef.current = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: "ping" })); + } + }, 30000); + }); + + ws.addEventListener("message", (event) => { + try { + const msg = JSON.parse(event.data); + if (msg.type === "data") { + if (typeof msg.data === "string") { + terminal.write(msg.data); + } else { + terminal.write(String(msg.data)); + } + } else if (msg.type === "error") + terminal.writeln(`\r\n[${t("terminal.error")}] ${msg.message}`); + else if (msg.type === "connected") { + isConnectingRef.current = false; + } else if (msg.type === "disconnected") { + wasDisconnectedBySSH.current = true; + isConnectingRef.current = false; + terminal.writeln( + `\r\n[${msg.message || t("terminal.disconnected")}]`, + ); + } + } catch { + // Ignore message parsing errors + } + }); + + ws.addEventListener("close", (event) => { + isConnectingRef.current = false; + + if (event.code === 1008) { + console.error("WebSocket authentication failed:", event.reason); + terminal.writeln(`\r\n[Authentication failed - please re-login]`); + + localStorage.removeItem("jwt"); + return; + } + + if (!wasDisconnectedBySSH.current) { + terminal.writeln(`\r\n[${t("terminal.connectionClosed")}]`); + } + }); + + ws.addEventListener("error", () => { + isConnectingRef.current = false; + terminal.writeln(`\r\n[${t("terminal.connectionError")}]`); + }); + } + + useEffect(() => { + if (!terminal || !xtermRef.current || !hostConfig) return; + + if (!isAuthenticated) { + return; + } + + terminal.options = { + cursorBlink: false, + cursorStyle: "bar", + scrollback: 10000, + fontSize: 14, + fontFamily: + '"Caskaydia Cove Nerd Font Mono", "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace', + theme: { background: "#09090b", foreground: "#f7f7f7" }, + allowTransparency: true, + convertEol: true, + windowsMode: false, + macOptionIsMeta: false, + macOptionClickForcesSelection: false, + rightClickSelectsWord: false, + fastScrollModifier: "alt", + fastScrollSensitivity: 5, + allowProposedApi: true, + disableStdin: true, + cursorInactiveStyle: "bar", + minimumContrastRatio: 1, + letterSpacing: 0, + lineHeight: 1.2, + }; + + const fitAddon = new FitAddon(); + const clipboardAddon = new ClipboardAddon(); + const unicode11Addon = new Unicode11Addon(); + const webLinksAddon = new WebLinksAddon(); + + fitAddonRef.current = fitAddon; + terminal.loadAddon(fitAddon); + terminal.loadAddon(clipboardAddon); + terminal.loadAddon(unicode11Addon); + terminal.loadAddon(webLinksAddon); + + terminal.unicode.activeVersion = "11"; + + terminal.open(xtermRef.current); + + const textarea = xtermRef.current.querySelector( + ".xterm-helper-textarea", + ) as HTMLTextAreaElement | null; + if (textarea) { + textarea.readOnly = true; + textarea.blur(); + } + + terminal.focus = () => {}; + + const resizeObserver = new ResizeObserver(() => { + if (resizeTimeout.current) clearTimeout(resizeTimeout.current); + resizeTimeout.current = setTimeout(() => { + if (!isVisibleRef.current) return; + fitAddonRef.current?.fit(); + if (terminal) scheduleNotify(terminal.cols, terminal.rows); + hardRefresh(); + }, 150); + }); + + resizeObserver.observe(xtermRef.current); + + const readyFonts = + (document as { fonts?: { ready?: Promise } }).fonts + ?.ready instanceof Promise + ? (document as { fonts?: { ready?: Promise } }).fonts.ready + : Promise.resolve(); + setVisible(true); + + readyFonts.then(() => { + setTimeout(() => { + fitAddon.fit(); + if (terminal) scheduleNotify(terminal.cols, terminal.rows); + hardRefresh(); + + const jwtToken = getCookie("jwt"); + if (!jwtToken || jwtToken.trim() === "") { + setIsConnected(false); + setIsConnecting(false); + setConnectionError("Authentication required"); + return; + } + + const cols = terminal.cols; + const rows = terminal.rows; + + const isDev = + process.env.NODE_ENV === "development" && + (window.location.port === "3000" || + window.location.port === "5173" || + window.location.port === ""); + + const baseWsUrl = isDev + ? `${window.location.protocol === "https:" ? "wss" : "ws"}://localhost:30002` + : isElectron() + ? (() => { + const baseUrl = + (window as { configuredServerUrl?: string }) + .configuredServerUrl || "http://127.0.0.1:30001"; + const wsProtocol = baseUrl.startsWith("https://") + ? "wss://" + : "ws://"; + const wsHost = baseUrl.replace(/^https?:\/\//, ""); + return `${wsProtocol}${wsHost}/ssh/websocket/`; + })() + : `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/ssh/websocket/`; + + if (isConnectingRef.current) { + return; + } + + isConnectingRef.current = true; + + if ( + webSocketRef.current && + webSocketRef.current.readyState !== WebSocket.CLOSED + ) { + webSocketRef.current.close(); + } + + if (pingIntervalRef.current) { + clearInterval(pingIntervalRef.current); + pingIntervalRef.current = null; + } + + const wsUrl = `${baseWsUrl}?token=${encodeURIComponent(jwtToken)}`; + + setIsConnecting(true); + setConnectionError(null); + + const ws = new WebSocket(wsUrl); + webSocketRef.current = ws; + wasDisconnectedBySSH.current = false; + + setupWebSocketListeners(ws, cols, rows); + }, 200); + }); + + return () => { + resizeObserver.disconnect(); + if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current); + if (resizeTimeout.current) clearTimeout(resizeTimeout.current); if (pingIntervalRef.current) { clearInterval(pingIntervalRef.current); pingIntervalRef.current = null; } webSocketRef.current?.close(); - }, - fit: () => { - fitAddonRef.current?.fit(); - if (terminal) scheduleNotify(terminal.cols, terminal.rows); - hardRefresh(); - }, - sendInput: (data: string) => { - if (webSocketRef.current?.readyState === 1) { - webSocketRef.current.send(JSON.stringify({ type: "input", data })); - } - }, - notifyResize: () => { - try { - const cols = terminal?.cols ?? undefined; - const rows = terminal?.rows ?? undefined; - if (typeof cols === "number" && typeof rows === "number") { - scheduleNotify(cols, rows); - hardRefresh(); - } - } catch { - // Ignore resize notification errors - } - }, - refresh: () => hardRefresh(), - }), - [terminal], - ); + }; + }, [xtermRef, terminal, hostConfig]); - function handleWindowResize() { - if (!isVisibleRef.current) return; - fitAddonRef.current?.fit(); - if (terminal) scheduleNotify(terminal.cols, terminal.rows); - hardRefresh(); - } - - function setupWebSocketListeners(ws: WebSocket, cols: number, rows: number) { - ws.addEventListener("open", () => { - ws.send( - JSON.stringify({ - type: "connectToHost", - data: { cols, rows, hostConfig }, - }), - ); - terminal.onData((data) => { - ws.send(JSON.stringify({ type: "input", data })); - }); - - pingIntervalRef.current = setInterval(() => { - if (ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ type: "ping" })); - } - }, 30000); - }); - - ws.addEventListener("message", (event) => { - try { - const msg = JSON.parse(event.data); - if (msg.type === "data") { - if (typeof msg.data === "string") { - terminal.write(msg.data); - } else { - terminal.write(String(msg.data)); - } - } else if (msg.type === "error") - terminal.writeln(`\r\n[${t("terminal.error")}] ${msg.message}`); - else if (msg.type === "connected") { - isConnectingRef.current = false; - } else if (msg.type === "disconnected") { - wasDisconnectedBySSH.current = true; - isConnectingRef.current = false; - terminal.writeln( - `\r\n[${msg.message || t("terminal.disconnected")}]`, - ); - } - } catch { - // Ignore message parsing errors + useEffect(() => { + if (isVisible && fitAddonRef.current) { + setTimeout(() => { + fitAddonRef.current?.fit(); + if (terminal) scheduleNotify(terminal.cols, terminal.rows); + hardRefresh(); + }, 0); } - }); + }, [isVisible, terminal]); - ws.addEventListener("close", (event) => { - isConnectingRef.current = false; - - if (event.code === 1008) { - console.error("WebSocket authentication failed:", event.reason); - terminal.writeln(`\r\n[Authentication failed - please re-login]`); - - localStorage.removeItem("jwt"); - return; - } - - if (!wasDisconnectedBySSH.current) { - terminal.writeln(`\r\n[${t("terminal.connectionClosed")}]`); - } - }); - - ws.addEventListener("error", () => { - isConnectingRef.current = false; - terminal.writeln(`\r\n[${t("terminal.connectionError")}]`); - }); - } - - useEffect(() => { - if (!terminal || !xtermRef.current || !hostConfig) return; - - if (!isAuthenticated) { - return; - } - - terminal.options = { - cursorBlink: false, - cursorStyle: "bar", - scrollback: 10000, - fontSize: 14, - fontFamily: - '"Caskaydia Cove Nerd Font Mono", "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace', - theme: { background: "#09090b", foreground: "#f7f7f7" }, - allowTransparency: true, - convertEol: true, - windowsMode: false, - macOptionIsMeta: false, - macOptionClickForcesSelection: false, - rightClickSelectsWord: false, - fastScrollModifier: "alt", - fastScrollSensitivity: 5, - allowProposedApi: true, - disableStdin: true, - cursorInactiveStyle: "bar", - minimumContrastRatio: 1, - letterSpacing: 0, - lineHeight: 1.2, - }; - - const fitAddon = new FitAddon(); - const clipboardAddon = new ClipboardAddon(); - const unicode11Addon = new Unicode11Addon(); - const webLinksAddon = new WebLinksAddon(); - - fitAddonRef.current = fitAddon; - terminal.loadAddon(fitAddon); - terminal.loadAddon(clipboardAddon); - terminal.loadAddon(unicode11Addon); - terminal.loadAddon(webLinksAddon); - - terminal.unicode.activeVersion = "11"; - - terminal.open(xtermRef.current); - - const textarea = xtermRef.current.querySelector( - ".xterm-helper-textarea", - ) as HTMLTextAreaElement | null; - if (textarea) { - textarea.readOnly = true; - textarea.blur(); - } - - terminal.focus = () => {}; - - const resizeObserver = new ResizeObserver(() => { - if (resizeTimeout.current) clearTimeout(resizeTimeout.current); - resizeTimeout.current = setTimeout(() => { - if (!isVisibleRef.current) return; - fitAddonRef.current?.fit(); - if (terminal) scheduleNotify(terminal.cols, terminal.rows); - hardRefresh(); - }, 150); - }); - - resizeObserver.observe(xtermRef.current); - - const readyFonts = - (document as any).fonts?.ready instanceof Promise - ? (document as any).fonts.ready - : Promise.resolve(); - setVisible(true); - - readyFonts.then(() => { - setTimeout(() => { - fitAddon.fit(); - if (terminal) scheduleNotify(terminal.cols, terminal.rows); - hardRefresh(); - - const jwtToken = getCookie("jwt"); - if (!jwtToken || jwtToken.trim() === "") { - setIsConnected(false); - setIsConnecting(false); - setConnectionError("Authentication required"); - return; - } - - const cols = terminal.cols; - const rows = terminal.rows; - - const isDev = - process.env.NODE_ENV === "development" && - (window.location.port === "3000" || - window.location.port === "5173" || - window.location.port === ""); - - const baseWsUrl = isDev - ? `${window.location.protocol === "https:" ? "wss" : "ws"}://localhost:30002` - : isElectron() - ? (() => { - const baseUrl = - (window as any).configuredServerUrl || - "http://127.0.0.1:30001"; - const wsProtocol = baseUrl.startsWith("https://") - ? "wss://" - : "ws://"; - const wsHost = baseUrl.replace(/^https?:\/\//, ""); - return `${wsProtocol}${wsHost}/ssh/websocket/`; - })() - : `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/ssh/websocket/`; - - if (isConnectingRef.current) { - return; - } - - isConnectingRef.current = true; - - if ( - webSocketRef.current && - webSocketRef.current.readyState !== WebSocket.CLOSED - ) { - webSocketRef.current.close(); - } - - if (pingIntervalRef.current) { - clearInterval(pingIntervalRef.current); - pingIntervalRef.current = null; - } - - const wsUrl = `${baseWsUrl}?token=${encodeURIComponent(jwtToken)}`; - - setIsConnecting(true); - setConnectionError(null); - - const ws = new WebSocket(wsUrl); - webSocketRef.current = ws; - wasDisconnectedBySSH.current = false; - - setupWebSocketListeners(ws, cols, rows); - }, 200); - }); - - return () => { - resizeObserver.disconnect(); - if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current); - if (resizeTimeout.current) clearTimeout(resizeTimeout.current); - if (pingIntervalRef.current) { - clearInterval(pingIntervalRef.current); - pingIntervalRef.current = null; - } - webSocketRef.current?.close(); - }; - }, [xtermRef, terminal, hostConfig]); - - useEffect(() => { - if (isVisible && fitAddonRef.current) { + useEffect(() => { + if (!fitAddonRef.current) return; setTimeout(() => { fitAddonRef.current?.fit(); if (terminal) scheduleNotify(terminal.cols, terminal.rows); hardRefresh(); }, 0); - } - }, [isVisible, terminal]); + }, [isVisible, terminal]); - useEffect(() => { - if (!fitAddonRef.current) return; - setTimeout(() => { - fitAddonRef.current?.fit(); - if (terminal) scheduleNotify(terminal.cols, terminal.rows); - hardRefresh(); - }, 0); - }, [isVisible, terminal]); - - return ( -
- ); -}); + return ( +
+ ); + }, +); const style = document.createElement("style"); style.innerHTML = ` diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index 16e1b553..2294d733 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -95,8 +95,22 @@ interface OIDCAuthorize { export function isElectron(): boolean { return ( - (window as any).IS_ELECTRON === true || - (window as any).electronAPI?.isElectron === true + ( + window as Window & + typeof globalThis & { + IS_ELECTRON?: boolean; + electronAPI?: unknown; + configuredServerUrl?: string; + } + ).IS_ELECTRON === true || + ( + window as Window & + typeof globalThis & { + IS_ELECTRON?: boolean; + electronAPI?: unknown; + configuredServerUrl?: string; + } + ).electronAPI?.isElectron === true ); } @@ -154,8 +168,8 @@ function createApiInstance( const startTime = performance.now(); const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - (config as any).startTime = startTime; - (config as any).requestId = requestId; + (config as Record).startTime = startTime; + (config as Record).requestId = requestId; const method = config.method?.toUpperCase() || "UNKNOWN"; const url = config.url || "UNKNOWN"; @@ -189,8 +203,8 @@ function createApiInstance( instance.interceptors.response.use( (response) => { const endTime = performance.now(); - const startTime = (response.config as any).startTime; - const requestId = (response.config as any).requestId; + const startTime = (response.config as Record).startTime; + const requestId = (response.config as Record).requestId; const responseTime = Math.round(endTime - startTime); const method = response.config.method?.toUpperCase() || "UNKNOWN"; @@ -227,8 +241,10 @@ function createApiInstance( }, (error: AxiosError) => { const endTime = performance.now(); - const startTime = (error.config as any)?.startTime; - const requestId = (error.config as any)?.requestId; + const startTime = (error.config as Record | undefined) + ?.startTime; + const requestId = (error.config as Record | undefined) + ?.requestId; const responseTime = startTime ? Math.round(endTime - startTime) : undefined; @@ -238,10 +254,11 @@ function createApiInstance( const fullUrl = error.config ? `${error.config.baseURL}${url}` : url; const status = error.response?.status; const message = - (error.response?.data as any)?.error || + (error.response?.data as Record)?.error || (error as Error).message || "Unknown error"; - const errorCode = (error.response?.data as any)?.code || error.code; + const errorCode = + (error.response?.data as Record)?.code || error.code; const context: LogContext = { requestId, @@ -274,7 +291,8 @@ function createApiInstance( } if (status === 401) { - const errorCode = (error.response?.data as any)?.code; + const errorCode = (error.response?.data as Record) + ?.code; const isSessionExpired = errorCode === "SESSION_EXPIRED"; if (isElectron()) { @@ -337,9 +355,14 @@ export async function getServerConfig(): Promise { if (!isElectron()) return null; try { - const result = await (window as any).electronAPI?.invoke( - "get-server-config", - ); + const result = await ( + window as Window & + typeof globalThis & { + IS_ELECTRON?: boolean; + electronAPI?: unknown; + configuredServerUrl?: string; + } + ).electronAPI?.invoke("get-server-config"); return result; } catch (error) { console.error("Failed to get server config:", error); @@ -351,13 +374,24 @@ export async function saveServerConfig(config: ServerConfig): Promise { if (!isElectron()) return false; try { - const result = await (window as any).electronAPI?.invoke( - "save-server-config", - config, - ); + const result = await ( + window as Window & + typeof globalThis & { + IS_ELECTRON?: boolean; + electronAPI?: unknown; + configuredServerUrl?: string; + } + ).electronAPI?.invoke("save-server-config", config); if (result?.success) { configuredServerUrl = config.serverUrl; - (window as any).configuredServerUrl = configuredServerUrl; + ( + window as Window & + typeof globalThis & { + IS_ELECTRON?: boolean; + electronAPI?: unknown; + configuredServerUrl?: string; + } + ).configuredServerUrl = configuredServerUrl; updateApiInstances(); return true; } @@ -375,10 +409,14 @@ export async function testServerConnection( return { success: false, error: "Not in Electron environment" }; try { - const result = await (window as any).electronAPI?.invoke( - "test-server-connection", - serverUrl, - ); + const result = await ( + window as Window & + typeof globalThis & { + IS_ELECTRON?: boolean; + electronAPI?: unknown; + configuredServerUrl?: string; + } + ).electronAPI?.invoke("test-server-connection", serverUrl); return result; } catch (error) { console.error("Failed to test server connection:", error); @@ -406,9 +444,14 @@ export async function checkElectronUpdate(): Promise<{ return { success: false, error: "Not in Electron environment" }; try { - const result = await (window as any).electronAPI?.invoke( - "check-electron-update", - ); + const result = await ( + window as Window & + typeof globalThis & { + IS_ELECTRON?: boolean; + electronAPI?: unknown; + configuredServerUrl?: string; + } + ).electronAPI?.invoke("check-electron-update"); return result; } catch (error) { console.error("Failed to check Electron update:", error); @@ -472,7 +515,14 @@ if (isElectron()) { .then((config) => { if (config?.serverUrl) { configuredServerUrl = config.serverUrl; - (window as any).configuredServerUrl = configuredServerUrl; + ( + window as Window & + typeof globalThis & { + IS_ELECTRON?: boolean; + electronAPI?: unknown; + configuredServerUrl?: string; + } + ).configuredServerUrl = configuredServerUrl; } initializeApiInstances(); }) @@ -495,7 +545,14 @@ function updateApiInstances() { initializeApiInstances(); - (window as any).configuredServerUrl = configuredServerUrl; + ( + window as Window & + typeof globalThis & { + IS_ELECTRON?: boolean; + electronAPI?: unknown; + configuredServerUrl?: string; + } + ).configuredServerUrl = configuredServerUrl; systemLogger.success("All API instances updated successfully", { operation: "api_instance_update_complete", @@ -564,7 +621,7 @@ function handleApiError(error: unknown, operation: string): never { 403, code || "ACCESS_DENIED", ); - (apiError as any).response = error.response; + (apiError as ApiError & { response?: unknown }).response = error.response; throw apiError; } else if (status === 404) { apiLogger.warn(`Not found: ${method} ${url}`, errorContext); @@ -788,7 +845,9 @@ export async function bulkImportSSHHosts(hosts: SSHHostData[]): Promise<{ } } -export async function deleteSSHHost(hostId: number): Promise { +export async function deleteSSHHost( + hostId: number, +): Promise> { try { const response = await sshHostApi.delete(`/db/host/${hostId}`); return response.data; @@ -821,7 +880,9 @@ export async function exportSSHHostWithCredentials( // SSH AUTOSTART MANAGEMENT // ============================================================================ -export async function enableAutoStart(sshConfigId: number): Promise { +export async function enableAutoStart( + sshConfigId: number, +): Promise> { try { const response = await sshHostApi.post("/autostart/enable", { sshConfigId, @@ -832,7 +893,9 @@ export async function enableAutoStart(sshConfigId: number): Promise { } } -export async function disableAutoStart(sshConfigId: number): Promise { +export async function disableAutoStart( + sshConfigId: number, +): Promise> { try { const response = await sshHostApi.delete("/autostart/disable", { data: { sshConfigId }, @@ -883,7 +946,9 @@ export async function getTunnelStatusByName( return statuses[tunnelName]; } -export async function connectTunnel(tunnelConfig: TunnelConfig): Promise { +export async function connectTunnel( + tunnelConfig: TunnelConfig, +): Promise> { try { const response = await tunnelApi.post("/tunnel/connect", tunnelConfig); return response.data; @@ -892,7 +957,9 @@ export async function connectTunnel(tunnelConfig: TunnelConfig): Promise { } } -export async function disconnectTunnel(tunnelName: string): Promise { +export async function disconnectTunnel( + tunnelName: string, +): Promise> { try { const response = await tunnelApi.post("/tunnel/disconnect", { tunnelName }); return response.data; @@ -901,7 +968,9 @@ export async function disconnectTunnel(tunnelName: string): Promise { } } -export async function cancelTunnel(tunnelName: string): Promise { +export async function cancelTunnel( + tunnelName: string, +): Promise> { try { const response = await tunnelApi.post("/tunnel/cancel", { tunnelName }); return response.data; @@ -929,7 +998,7 @@ export async function getFileManagerRecent( export async function addFileManagerRecent( file: FileManagerOperation, -): Promise { +): Promise> { try { const response = await sshHostApi.post("/file_manager/recent", file); return response.data; @@ -940,7 +1009,7 @@ export async function addFileManagerRecent( export async function removeFileManagerRecent( file: FileManagerOperation, -): Promise { +): Promise> { try { const response = await sshHostApi.delete("/file_manager/recent", { data: file, @@ -966,7 +1035,7 @@ export async function getFileManagerPinned( export async function addFileManagerPinned( file: FileManagerOperation, -): Promise { +): Promise> { try { const response = await sshHostApi.post("/file_manager/pinned", file); return response.data; @@ -977,7 +1046,7 @@ export async function addFileManagerPinned( export async function removeFileManagerPinned( file: FileManagerOperation, -): Promise { +): Promise> { try { const response = await sshHostApi.delete("/file_manager/pinned", { data: file, @@ -1003,7 +1072,7 @@ export async function getFileManagerShortcuts( export async function addFileManagerShortcut( shortcut: FileManagerOperation, -): Promise { +): Promise> { try { const response = await sshHostApi.post("/file_manager/shortcuts", shortcut); return response.data; @@ -1014,7 +1083,7 @@ export async function addFileManagerShortcut( export async function removeFileManagerShortcut( shortcut: FileManagerOperation, -): Promise { +): Promise> { try { const response = await sshHostApi.delete("/file_manager/shortcuts", { data: shortcut, @@ -1043,7 +1112,7 @@ export async function connectSSH( credentialId?: number; userId?: string; }, -): Promise { +): Promise> { try { const response = await fileManagerApi.post("/ssh/connect", { sessionId, @@ -1055,7 +1124,9 @@ export async function connectSSH( } } -export async function disconnectSSH(sessionId: string): Promise { +export async function disconnectSSH( + sessionId: string, +): Promise> { try { const response = await fileManagerApi.post("/ssh/disconnect", { sessionId, @@ -1069,7 +1140,7 @@ export async function disconnectSSH(sessionId: string): Promise { export async function verifySSHTOTP( sessionId: string, totpCode: string, -): Promise { +): Promise> { try { const response = await fileManagerApi.post("/ssh/connect-totp", { sessionId, @@ -1094,7 +1165,9 @@ export async function getSSHStatus( } } -export async function keepSSHAlive(sessionId: string): Promise { +export async function keepSSHAlive( + sessionId: string, +): Promise> { try { const response = await fileManagerApi.post("/ssh/keepalive", { sessionId, @@ -1108,7 +1181,7 @@ export async function keepSSHAlive(sessionId: string): Promise { export async function listSSHFiles( sessionId: string, path: string, -): Promise<{ files: any[]; path: string }> { +): Promise<{ files: unknown[]; path: string }> { try { const response = await fileManagerApi.get("/ssh/listFiles", { params: { sessionId, path }, @@ -1143,12 +1216,15 @@ export async function readSSHFile( params: { sessionId, path }, }); return response.data; - } catch (error: any) { + } catch (error: unknown) { if (error.response?.status === 404) { const customError = new Error("File not found"); - (customError as any).response = error.response; - (customError as any).isFileNotFound = - error.response.data?.fileNotFound || true; + ( + customError as Error & { response?: unknown; isFileNotFound?: boolean } + ).response = error.response; + ( + customError as Error & { response?: unknown; isFileNotFound?: boolean } + ).isFileNotFound = error.response.data?.fileNotFound || true; throw customError; } handleApiError(error, "read SSH file"); @@ -1161,7 +1237,7 @@ export async function writeSSHFile( content: string, hostId?: number, userId?: string, -): Promise { +): Promise> { try { const response = await fileManagerApi.post("/ssh/writeFile", { sessionId, @@ -1192,7 +1268,7 @@ export async function uploadSSHFile( content: string, hostId?: number, userId?: string, -): Promise { +): Promise> { try { const response = await fileManagerApi.post("/ssh/uploadFile", { sessionId, @@ -1213,7 +1289,7 @@ export async function downloadSSHFile( filePath: string, hostId?: number, userId?: string, -): Promise { +): Promise> { try { const response = await fileManagerApi.post("/ssh/downloadFile", { sessionId, @@ -1234,7 +1310,7 @@ export async function createSSHFile( content: string = "", hostId?: number, userId?: string, -): Promise { +): Promise> { try { const response = await fileManagerApi.post("/ssh/createFile", { sessionId, @@ -1256,7 +1332,7 @@ export async function createSSHFolder( folderName: string, hostId?: number, userId?: string, -): Promise { +): Promise> { try { const response = await fileManagerApi.post("/ssh/createFolder", { sessionId, @@ -1277,7 +1353,7 @@ export async function deleteSSHItem( isDirectory: boolean, hostId?: number, userId?: string, -): Promise { +): Promise> { try { const response = await fileManagerApi.delete("/ssh/deleteItem", { data: { @@ -1300,7 +1376,7 @@ export async function copySSHItem( targetDir: string, hostId?: number, userId?: string, -): Promise { +): Promise> { try { const response = await fileManagerApi.post( "/ssh/copyItem", @@ -1328,7 +1404,7 @@ export async function renameSSHItem( newName: string, hostId?: number, userId?: string, -): Promise { +): Promise> { try { const response = await fileManagerApi.put("/ssh/renameItem", { sessionId, @@ -1350,7 +1426,7 @@ export async function moveSSHItem( newPath: string, hostId?: number, userId?: string, -): Promise { +): Promise> { try { const response = await fileManagerApi.put( "/ssh/moveItem", @@ -1377,7 +1453,9 @@ export async function moveSSHItem( // ============================================================================ // Recent Files -export async function getRecentFiles(hostId: number): Promise { +export async function getRecentFiles( + hostId: number, +): Promise> { try { const response = await authApi.get("/ssh/file_manager/recent", { params: { hostId }, @@ -1393,7 +1471,7 @@ export async function addRecentFile( hostId: number, path: string, name?: string, -): Promise { +): Promise> { try { const response = await authApi.post("/ssh/file_manager/recent", { hostId, @@ -1410,7 +1488,7 @@ export async function addRecentFile( export async function removeRecentFile( hostId: number, path: string, -): Promise { +): Promise> { try { const response = await authApi.delete("/ssh/file_manager/recent", { data: { hostId, path }, @@ -1422,7 +1500,9 @@ export async function removeRecentFile( } } -export async function getPinnedFiles(hostId: number): Promise { +export async function getPinnedFiles( + hostId: number, +): Promise> { try { const response = await authApi.get("/ssh/file_manager/pinned", { params: { hostId }, @@ -1438,7 +1518,7 @@ export async function addPinnedFile( hostId: number, path: string, name?: string, -): Promise { +): Promise> { try { const response = await authApi.post("/ssh/file_manager/pinned", { hostId, @@ -1455,7 +1535,7 @@ export async function addPinnedFile( export async function removePinnedFile( hostId: number, path: string, -): Promise { +): Promise> { try { const response = await authApi.delete("/ssh/file_manager/pinned", { data: { hostId, path }, @@ -1467,7 +1547,9 @@ export async function removePinnedFile( } } -export async function getFolderShortcuts(hostId: number): Promise { +export async function getFolderShortcuts( + hostId: number, +): Promise> { try { const response = await authApi.get("/ssh/file_manager/shortcuts", { params: { hostId }, @@ -1483,7 +1565,7 @@ export async function addFolderShortcut( hostId: number, path: string, name?: string, -): Promise { +): Promise> { try { const response = await authApi.post("/ssh/file_manager/shortcuts", { hostId, @@ -1500,7 +1582,7 @@ export async function addFolderShortcut( export async function removeFolderShortcut( hostId: number, path: string, -): Promise { +): Promise> { try { const response = await authApi.delete("/ssh/file_manager/shortcuts", { data: { hostId, path }, @@ -1552,7 +1634,7 @@ export async function getServerMetricsById(id: number): Promise { export async function registerUser( username: string, password: string, -): Promise { +): Promise> { try { const response = await authApi.post("/users/create", { username, @@ -1638,11 +1720,11 @@ export async function getPasswordLoginAllowed(): Promise<{ allowed: boolean }> { } } -export async function getOIDCConfig(): Promise { +export async function getOIDCConfig(): Promise> { try { const response = await authApi.get("/users/oidc-config"); return response.data; - } catch (error: any) { + } catch (error: unknown) { console.warn( "Failed to fetch OIDC config:", error.response?.data?.error || error.message, @@ -1669,7 +1751,9 @@ export async function getUserCount(): Promise { } } -export async function initiatePasswordReset(username: string): Promise { +export async function initiatePasswordReset( + username: string, +): Promise> { try { const response = await authApi.post("/users/initiate-reset", { username }); return response.data; @@ -1681,7 +1765,7 @@ export async function initiatePasswordReset(username: string): Promise { export async function verifyPasswordResetCode( username: string, resetCode: string, -): Promise { +): Promise> { try { const response = await authApi.post("/users/verify-reset-code", { username, @@ -1697,7 +1781,7 @@ export async function completePasswordReset( username: string, tempToken: string, newPassword: string, -): Promise { +): Promise> { try { const response = await authApi.post("/users/complete-reset", { username, @@ -1732,7 +1816,9 @@ export async function getUserList(): Promise<{ users: UserInfo[] }> { } } -export async function makeUserAdmin(username: string): Promise { +export async function makeUserAdmin( + username: string, +): Promise> { try { const response = await authApi.post("/users/make-admin", { username }); return response.data; @@ -1741,7 +1827,9 @@ export async function makeUserAdmin(username: string): Promise { } } -export async function removeAdminStatus(username: string): Promise { +export async function removeAdminStatus( + username: string, +): Promise> { try { const response = await authApi.post("/users/remove-admin", { username }); return response.data; @@ -1750,7 +1838,9 @@ export async function removeAdminStatus(username: string): Promise { } } -export async function deleteUser(username: string): Promise { +export async function deleteUser( + username: string, +): Promise> { try { const response = await authApi.delete("/users/delete-user", { data: { username }, @@ -1761,7 +1851,9 @@ export async function deleteUser(username: string): Promise { } } -export async function deleteAccount(password: string): Promise { +export async function deleteAccount( + password: string, +): Promise> { try { const response = await authApi.delete("/users/delete-account", { data: { password }, @@ -1774,7 +1866,7 @@ export async function deleteAccount(password: string): Promise { export async function updateRegistrationAllowed( allowed: boolean, -): Promise { +): Promise> { try { const response = await authApi.patch("/users/registration-allowed", { allowed, @@ -1798,7 +1890,9 @@ export async function updatePasswordLoginAllowed( } } -export async function updateOIDCConfig(config: any): Promise { +export async function updateOIDCConfig( + config: Record, +): Promise> { try { const response = await authApi.post("/users/oidc-config", config); return response.data; @@ -1807,7 +1901,7 @@ export async function updateOIDCConfig(config: any): Promise { } } -export async function disableOIDCConfig(): Promise { +export async function disableOIDCConfig(): Promise> { try { const response = await authApi.delete("/users/oidc-config"); return response.data; @@ -1893,7 +1987,9 @@ export async function generateBackupCodes( } } -export async function getUserAlerts(): Promise<{ alerts: any[] }> { +export async function getUserAlerts(): Promise<{ + alerts: Array>; +}> { try { const response = await authApi.get(`/alerts`); return response.data; @@ -1902,7 +1998,9 @@ export async function getUserAlerts(): Promise<{ alerts: any[] }> { } } -export async function dismissAlert(alertId: string): Promise { +export async function dismissAlert( + alertId: string, +): Promise> { try { const response = await authApi.post("/alerts/dismiss", { alertId }); return response.data; @@ -1915,7 +2013,9 @@ export async function dismissAlert(alertId: string): Promise { // UPDATES & RELEASES // ============================================================================ -export async function getReleasesRSS(perPage: number = 100): Promise { +export async function getReleasesRSS( + perPage: number = 100, +): Promise> { try { const response = await authApi.get(`/releases/rss?per_page=${perPage}`); return response.data; @@ -1924,7 +2024,7 @@ export async function getReleasesRSS(perPage: number = 100): Promise { } } -export async function getVersionInfo(): Promise { +export async function getVersionInfo(): Promise> { try { const response = await authApi.get("/version"); return response.data; @@ -1937,7 +2037,7 @@ export async function getVersionInfo(): Promise { // DATABASE HEALTH // ============================================================================ -export async function getDatabaseHealth(): Promise { +export async function getDatabaseHealth(): Promise> { try { const response = await authApi.get("/users/db-health"); return response.data; @@ -1950,7 +2050,7 @@ export async function getDatabaseHealth(): Promise { // SSH CREDENTIALS MANAGEMENT // ============================================================================ -export async function getCredentials(): Promise { +export async function getCredentials(): Promise> { try { const response = await authApi.get("/credentials"); return response.data; @@ -1959,7 +2059,9 @@ export async function getCredentials(): Promise { } } -export async function getCredentialDetails(credentialId: number): Promise { +export async function getCredentialDetails( + credentialId: number, +): Promise> { try { const response = await authApi.get(`/credentials/${credentialId}`); return response.data; @@ -1968,7 +2070,9 @@ export async function getCredentialDetails(credentialId: number): Promise { } } -export async function createCredential(credentialData: any): Promise { +export async function createCredential( + credentialData: Record, +): Promise> { try { const response = await authApi.post("/credentials", credentialData); return response.data; @@ -1979,8 +2083,8 @@ export async function createCredential(credentialData: any): Promise { export async function updateCredential( credentialId: number, - credentialData: any, -): Promise { + credentialData: Record, +): Promise> { try { const response = await authApi.put( `/credentials/${credentialId}`, @@ -1992,7 +2096,9 @@ export async function updateCredential( } } -export async function deleteCredential(credentialId: number): Promise { +export async function deleteCredential( + credentialId: number, +): Promise> { try { const response = await authApi.delete(`/credentials/${credentialId}`); return response.data; @@ -2001,7 +2107,9 @@ export async function deleteCredential(credentialId: number): Promise { } } -export async function getCredentialHosts(credentialId: number): Promise { +export async function getCredentialHosts( + credentialId: number, +): Promise> { try { const response = await authApi.get(`/credentials/${credentialId}/hosts`); return response.data; @@ -2010,7 +2118,7 @@ export async function getCredentialHosts(credentialId: number): Promise { } } -export async function getCredentialFolders(): Promise { +export async function getCredentialFolders(): Promise> { try { const response = await authApi.get("/credentials/folders"); return response.data; @@ -2019,7 +2127,9 @@ export async function getCredentialFolders(): Promise { } } -export async function getSSHHostWithCredentials(hostId: number): Promise { +export async function getSSHHostWithCredentials( + hostId: number, +): Promise> { try { const response = await sshHostApi.get( `/db/host/${hostId}/with-credentials`, @@ -2033,7 +2143,7 @@ export async function getSSHHostWithCredentials(hostId: number): Promise { export async function applyCredentialToHost( hostId: number, credentialId: number, -): Promise { +): Promise> { try { const response = await sshHostApi.post( `/db/host/${hostId}/apply-credential`, @@ -2045,7 +2155,9 @@ export async function applyCredentialToHost( } } -export async function removeCredentialFromHost(hostId: number): Promise { +export async function removeCredentialFromHost( + hostId: number, +): Promise> { try { const response = await sshHostApi.delete(`/db/host/${hostId}/credential`); return response.data; @@ -2057,7 +2169,7 @@ export async function removeCredentialFromHost(hostId: number): Promise { export async function migrateHostToCredential( hostId: number, credentialName: string, -): Promise { +): Promise> { try { const response = await sshHostApi.post( `/db/host/${hostId}/migrate-to-credential`, @@ -2073,7 +2185,7 @@ export async function migrateHostToCredential( // SSH FOLDER MANAGEMENT // ============================================================================ -export async function getFoldersWithStats(): Promise { +export async function getFoldersWithStats(): Promise> { try { const response = await authApi.get("/ssh/db/folders/with-stats"); return response.data; @@ -2085,7 +2197,7 @@ export async function getFoldersWithStats(): Promise { export async function renameFolder( oldName: string, newName: string, -): Promise { +): Promise> { try { const response = await authApi.put("/ssh/folders/rename", { oldName, @@ -2100,7 +2212,7 @@ export async function renameFolder( export async function renameCredentialFolder( oldName: string, newName: string, -): Promise { +): Promise> { try { const response = await authApi.put("/credentials/folders/rename", { oldName, @@ -2115,7 +2227,7 @@ export async function renameCredentialFolder( export async function detectKeyType( privateKey: string, keyPassword?: string, -): Promise { +): Promise> { try { const response = await authApi.post("/credentials/detect-key-type", { privateKey, @@ -2127,7 +2239,9 @@ export async function detectKeyType( } } -export async function detectPublicKeyType(publicKey: string): Promise { +export async function detectPublicKeyType( + publicKey: string, +): Promise> { try { const response = await authApi.post("/credentials/detect-public-key-type", { publicKey, @@ -2142,7 +2256,7 @@ export async function validateKeyPair( privateKey: string, publicKey: string, keyPassword?: string, -): Promise { +): Promise> { try { const response = await authApi.post("/credentials/validate-key-pair", { privateKey, @@ -2158,7 +2272,7 @@ export async function validateKeyPair( export async function generatePublicKeyFromPrivate( privateKey: string, keyPassword?: string, -): Promise { +): Promise> { try { const response = await authApi.post("/credentials/generate-public-key", { privateKey, @@ -2174,7 +2288,7 @@ export async function generateKeyPair( keyType: "ssh-ed25519" | "ssh-rsa" | "ecdsa-sha2-nistp256", keySize?: number, passphrase?: string, -): Promise { +): Promise> { try { const response = await authApi.post("/credentials/generate-key-pair", { keyType, @@ -2190,7 +2304,7 @@ export async function generateKeyPair( export async function deployCredentialToHost( credentialId: number, targetHostId: number, -): Promise { +): Promise> { try { const response = await authApi.post( `/credentials/${credentialId}/deploy-to-host`, @@ -2206,7 +2320,7 @@ export async function deployCredentialToHost( // SNIPPETS API // ============================================================================ -export async function getSnippets(): Promise { +export async function getSnippets(): Promise> { try { const response = await authApi.get("/snippets"); return response.data; @@ -2215,7 +2329,9 @@ export async function getSnippets(): Promise { } } -export async function createSnippet(snippetData: any): Promise { +export async function createSnippet( + snippetData: Record, +): Promise> { try { const response = await authApi.post("/snippets", snippetData); return response.data; @@ -2226,8 +2342,8 @@ export async function createSnippet(snippetData: any): Promise { export async function updateSnippet( snippetId: number, - snippetData: any, -): Promise { + snippetData: Record, +): Promise> { try { const response = await authApi.put(`/snippets/${snippetId}`, snippetData); return response.data; @@ -2236,7 +2352,9 @@ export async function updateSnippet( } } -export async function deleteSnippet(snippetId: number): Promise { +export async function deleteSnippet( + snippetId: number, +): Promise> { try { const response = await authApi.delete(`/snippets/${snippetId}`); return response.data;