diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml index 38bccc88..3cc6d417 100644 --- a/.github/workflows/translate.yml +++ b/.github/workflows/translate.yml @@ -345,7 +345,33 @@ jobs: continue-on-error: true create-pr: - needs: [translate-zh, translate-ru, translate-pt, translate-fr, translate-es, translate-de, translate-hi, translate-bn, translate-ja, translate-vi, translate-tr, translate-ko, translate-it, translate-he, translate-ar, translate-pl, translate-nl, translate-sv, translate-id, translate-th, translate-uk, translate-cs, translate-ro, translate-el] + needs: + [ + translate-zh, + translate-ru, + translate-pt, + translate-fr, + translate-es, + translate-de, + translate-hi, + translate-bn, + translate-ja, + translate-vi, + translate-tr, + translate-ko, + translate-it, + translate-he, + translate-ar, + translate-pl, + translate-nl, + translate-sv, + translate-id, + translate-th, + translate-uk, + translate-cs, + translate-ro, + translate-el, + ] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/electron/main.cjs b/electron/main.cjs index 9cd63d58..06dc9ea2 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -11,10 +11,8 @@ const fs = require("fs"); const os = require("os"); if (process.platform === "linux") { - // Enable Ozone platform auto-detection for Wayland/X11 support app.commandLine.appendSwitch("--ozone-platform-hint=auto"); - // Enable hardware video decoding if available app.commandLine.appendSwitch("--enable-features=VaapiVideoDecoder"); } diff --git a/electron/preload.js b/electron/preload.js index 1db1b356..ea1f3458 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -2,21 +2,6 @@ const { contextBridge, ipcRenderer } = require("electron"); contextBridge.exposeInMainWorld("electronAPI", { getAppVersion: () => ipcRenderer.invoke("get-app-version"), - getPlatform: () => ipcRenderer.invoke("get-platform"), - checkElectronUpdate: () => ipcRenderer.invoke("check-electron-update"), - - getServerConfig: () => ipcRenderer.invoke("get-server-config"), - saveServerConfig: (config) => - ipcRenderer.invoke("save-server-config", config), - testServerConnection: (serverUrl) => - ipcRenderer.invoke("test-server-connection", serverUrl), - - showSaveDialog: (options) => ipcRenderer.invoke("show-save-dialog", options), - showOpenDialog: (options) => ipcRenderer.invoke("show-open-dialog", options), - - onUpdateAvailable: (callback) => ipcRenderer.on("update-available", callback), - onUpdateDownloaded: (callback) => - ipcRenderer.on("update-downloaded", callback), removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel), isElectron: true, diff --git a/src/backend/dashboard.ts b/src/backend/dashboard.ts index 465ab9d8..5df48fb6 100644 --- a/src/backend/dashboard.ts +++ b/src/backend/dashboard.ts @@ -15,7 +15,7 @@ const authManager = AuthManager.getInstance(); const serverStartTime = Date.now(); const activityRateLimiter = new Map(); -const RATE_LIMIT_MS = 1000; // 1 second window +const RATE_LIMIT_MS = 1000; app.use( cors({ diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts index bb37f17a..0479b3e9 100644 --- a/src/backend/database/db/index.ts +++ b/src/backend/database/db/index.ts @@ -578,7 +578,6 @@ const migrateSchema = () => { addColumnIfNotExists("ssh_data", "notes", "TEXT"); - // SOCKS5 Proxy columns addColumnIfNotExists("ssh_data", "use_socks5", "INTEGER"); addColumnIfNotExists("ssh_data", "socks5_host", "TEXT"); addColumnIfNotExists("ssh_data", "socks5_port", "INTEGER"); @@ -590,7 +589,6 @@ const migrateSchema = () => { addColumnIfNotExists("ssh_credentials", "public_key", "TEXT"); addColumnIfNotExists("ssh_credentials", "detected_key_type", "TEXT"); - // System-encrypted fields for offline credential sharing addColumnIfNotExists("ssh_credentials", "system_password", "TEXT"); addColumnIfNotExists("ssh_credentials", "system_key", "TEXT"); addColumnIfNotExists("ssh_credentials", "system_key_password", "TEXT"); @@ -655,7 +653,6 @@ const migrateSchema = () => { } } - // RBAC Phase 1: Host Access table try { sqlite.prepare("SELECT id FROM host_access LIMIT 1").get(); } catch { @@ -678,9 +675,6 @@ const migrateSchema = () => { FOREIGN KEY (granted_by) REFERENCES users (id) ON DELETE CASCADE ); `); - databaseLogger.info("Created host_access table", { - operation: "schema_migration", - }); } catch (createError) { databaseLogger.warn("Failed to create host_access table", { operation: "schema_migration", @@ -689,15 +683,11 @@ const migrateSchema = () => { } } - // Migration: Add role_id column to existing host_access table try { sqlite.prepare("SELECT role_id FROM host_access LIMIT 1").get(); } catch { try { sqlite.exec("ALTER TABLE host_access ADD COLUMN role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE"); - databaseLogger.info("Added role_id column to host_access table", { - operation: "schema_migration", - }); } catch (alterError) { databaseLogger.warn("Failed to add role_id column", { operation: "schema_migration", @@ -706,15 +696,11 @@ const migrateSchema = () => { } } - // Migration: Add sudo_password column to ssh_data table try { sqlite.prepare("SELECT sudo_password FROM ssh_data LIMIT 1").get(); } catch { try { sqlite.exec("ALTER TABLE ssh_data ADD COLUMN sudo_password TEXT"); - databaseLogger.info("Added sudo_password column to ssh_data table", { - operation: "schema_migration", - }); } catch (alterError) { databaseLogger.warn("Failed to add sudo_password column", { operation: "schema_migration", @@ -723,7 +709,6 @@ const migrateSchema = () => { } } - // RBAC Phase 2: Roles tables try { sqlite.prepare("SELECT id FROM roles LIMIT 1").get(); } catch { @@ -740,9 +725,6 @@ const migrateSchema = () => { updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); `); - databaseLogger.info("Created roles table", { - operation: "schema_migration", - }); } catch (createError) { databaseLogger.warn("Failed to create roles table", { operation: "schema_migration", @@ -768,9 +750,6 @@ const migrateSchema = () => { FOREIGN KEY (granted_by) REFERENCES users (id) ON DELETE SET NULL ); `); - databaseLogger.info("Created user_roles table", { - operation: "schema_migration", - }); } catch (createError) { databaseLogger.warn("Failed to create user_roles table", { operation: "schema_migration", @@ -779,7 +758,6 @@ const migrateSchema = () => { } } - // RBAC Phase 3: Audit logging tables try { sqlite.prepare("SELECT id FROM audit_logs LIMIT 1").get(); } catch { @@ -802,9 +780,6 @@ const migrateSchema = () => { FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ); `); - databaseLogger.info("Created audit_logs table", { - operation: "schema_migration", - }); } catch (createError) { databaseLogger.warn("Failed to create audit_logs table", { operation: "schema_migration", @@ -836,9 +811,6 @@ const migrateSchema = () => { FOREIGN KEY (access_id) REFERENCES host_access (id) ON DELETE SET NULL ); `); - databaseLogger.info("Created session_recordings table", { - operation: "schema_migration", - }); } catch (createError) { databaseLogger.warn("Failed to create session_recordings table", { operation: "schema_migration", @@ -847,7 +819,6 @@ const migrateSchema = () => { } } - // RBAC: Shared Credentials table try { sqlite.prepare("SELECT id FROM shared_credentials LIMIT 1").get(); } catch { @@ -872,9 +843,6 @@ const migrateSchema = () => { FOREIGN KEY (target_user_id) REFERENCES users (id) ON DELETE CASCADE ); `); - databaseLogger.info("Created shared_credentials table", { - operation: "schema_migration", - }); } catch (createError) { databaseLogger.warn("Failed to create shared_credentials table", { operation: "schema_migration", @@ -883,51 +851,31 @@ const migrateSchema = () => { } } - // Clean up old system roles and seed correct ones try { - // First, check what roles exist const existingRoles = sqlite.prepare("SELECT name, is_system FROM roles").all() as Array<{ name: string; is_system: number }>; - databaseLogger.info("Current roles in database", { - operation: "schema_migration", - roles: existingRoles, - }); - // Migration: Remove ALL old unwanted roles (system or not) and keep only admin and user try { const validSystemRoles = ['admin', 'user']; const unwantedRoleNames = ['superAdmin', 'powerUser', 'readonly', 'member']; let deletedCount = 0; - // First delete known unwanted role names const deleteByName = sqlite.prepare("DELETE FROM roles WHERE name = ?"); for (const roleName of unwantedRoleNames) { const result = deleteByName.run(roleName); if (result.changes > 0) { deletedCount += result.changes; - databaseLogger.info(`Deleted role by name: ${roleName}`, { - operation: "schema_migration", - }); } } - // Then delete any system roles that are not admin or user const deleteOldSystemRole = sqlite.prepare("DELETE FROM roles WHERE name = ? AND is_system = 1"); for (const role of existingRoles) { if (role.is_system === 1 && !validSystemRoles.includes(role.name) && !unwantedRoleNames.includes(role.name)) { const result = deleteOldSystemRole.run(role.name); if (result.changes > 0) { deletedCount += result.changes; - databaseLogger.info(`Deleted system role: ${role.name}`, { - operation: "schema_migration", - }); } } } - - databaseLogger.info("Cleanup completed", { - operation: "schema_migration", - deletedCount, - }); } catch (cleanupError) { databaseLogger.warn("Failed to clean up old system roles", { operation: "schema_migration", @@ -935,7 +883,6 @@ const migrateSchema = () => { }); } - // Ensure only admin and user system roles exist const systemRoles = [ { name: "admin", @@ -954,7 +901,6 @@ const migrateSchema = () => { for (const role of systemRoles) { const existingRole = sqlite.prepare("SELECT id FROM roles WHERE name = ?").get(role.name); if (!existingRole) { - // Create if doesn't exist try { sqlite.prepare(` INSERT INTO roles (name, display_name, description, is_system, permissions) @@ -969,11 +915,6 @@ const migrateSchema = () => { } } - databaseLogger.info("System roles migration completed", { - operation: "schema_migration", - }); - - // Migrate existing is_admin users to roles try { const adminUsers = sqlite.prepare("SELECT id FROM users WHERE is_admin = 1").all() as { id: string }[]; const normalUsers = sqlite.prepare("SELECT id FROM users WHERE is_admin = 0").all() as { id: string }[]; @@ -994,11 +935,6 @@ const migrateSchema = () => { // Ignore duplicate errors } } - - databaseLogger.info("Migrated admin users to admin role", { - operation: "schema_migration", - count: adminUsers.length, - }); } if (userRole) { @@ -1014,11 +950,6 @@ const migrateSchema = () => { // Ignore duplicate errors } } - - databaseLogger.info("Migrated normal users to user role", { - operation: "schema_migration", - count: normalUsers.length, - }); } } catch (migrationError) { databaseLogger.warn("Failed to migrate existing users to roles", { diff --git a/src/backend/database/db/schema.ts b/src/backend/database/db/schema.ts index 08c9e034..71c07653 100644 --- a/src/backend/database/db/schema.ts +++ b/src/backend/database/db/schema.ts @@ -101,7 +101,7 @@ export const sshData = sqliteTable("ssh_data", { socks5Port: integer("socks5_port"), socks5Username: text("socks5_username"), socks5Password: text("socks5_password"), - socks5ProxyChain: text("socks5_proxy_chain"), // JSON array for proxy chains + socks5ProxyChain: text("socks5_proxy_chain"), createdAt: text("created_at") .notNull() @@ -186,7 +186,6 @@ export const sshCredentials = sqliteTable("ssh_credentials", { keyType: text("key_type"), detectedKeyType: text("detected_key_type"), - // System-encrypted fields for offline credential sharing systemPassword: text("system_password"), systemKey: text("system_key", { length: 16384 }), systemKeyPassword: text("system_key_password"), @@ -296,32 +295,27 @@ export const commandHistory = sqliteTable("command_history", { .default(sql`CURRENT_TIMESTAMP`), }); -// RBAC Phase 1: Host Sharing export const hostAccess = sqliteTable("host_access", { id: integer("id").primaryKey({ autoIncrement: true }), hostId: integer("host_id") .notNull() .references(() => sshData.id, { onDelete: "cascade" }), - // Share target: either userId OR roleId (at least one must be set) userId: text("user_id") - .references(() => users.id, { onDelete: "cascade" }), // Optional + .references(() => users.id, { onDelete: "cascade" }), roleId: integer("role_id") - .references(() => roles.id, { onDelete: "cascade" }), // Optional + .references(() => roles.id, { onDelete: "cascade" }), grantedBy: text("granted_by") .notNull() .references(() => users.id, { onDelete: "cascade" }), - // Permission level (view-only) permissionLevel: text("permission_level") .notNull() - .default("view"), // Only "view" is supported + .default("view"), - // Time-based access - expiresAt: text("expires_at"), // NULL = never expires + expiresAt: text("expires_at"), - // Metadata createdAt: text("created_at") .notNull() .default(sql`CURRENT_TIMESTAMP`), @@ -329,26 +323,21 @@ export const hostAccess = sqliteTable("host_access", { accessCount: integer("access_count").notNull().default(0), }); -// RBAC: Shared Credentials (per-user encrypted credential copies) export const sharedCredentials = sqliteTable("shared_credentials", { id: integer("id").primaryKey({ autoIncrement: true }), - // Link to the host access grant (CASCADE delete when share revoked) hostAccessId: integer("host_access_id") .notNull() .references(() => hostAccess.id, { onDelete: "cascade" }), - // Link to the original credential (for tracking updates/CASCADE delete) originalCredentialId: integer("original_credential_id") .notNull() .references(() => sshCredentials.id, { onDelete: "cascade" }), - // Target user (recipient of the share) - CASCADE delete when user deleted targetUserId: text("target_user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), - // Encrypted credential data (encrypted with targetUserId's DEK) encryptedUsername: text("encrypted_username").notNull(), encryptedAuthType: text("encrypted_auth_type").notNull(), encryptedPassword: text("encrypted_password"), @@ -356,7 +345,6 @@ export const sharedCredentials = sqliteTable("shared_credentials", { encryptedKeyPassword: text("encrypted_key_password"), encryptedKeyType: text("encrypted_key_type"), - // Metadata createdAt: text("created_at") .notNull() .default(sql`CURRENT_TIMESTAMP`), @@ -364,26 +352,22 @@ export const sharedCredentials = sqliteTable("shared_credentials", { .notNull() .default(sql`CURRENT_TIMESTAMP`), - // Track if needs re-encryption (when original credential updated but target user offline) needsReEncryption: integer("needs_re_encryption", { mode: "boolean" }) .notNull() .default(false), }); -// RBAC Phase 2: Roles export const roles = sqliteTable("roles", { id: integer("id").primaryKey({ autoIncrement: true }), name: text("name").notNull().unique(), - displayName: text("display_name").notNull(), // For i18n + displayName: text("display_name").notNull(), description: text("description"), - // System roles cannot be deleted isSystem: integer("is_system", { mode: "boolean" }) .notNull() .default(false), - // Permissions stored as JSON array (optional - used for grouping only in current phase) - permissions: text("permissions"), // ["hosts.*", "credentials.read", ...] - optional + permissions: text("permissions"), createdAt: text("created_at") .notNull() @@ -410,32 +394,26 @@ export const userRoles = sqliteTable("user_roles", { .default(sql`CURRENT_TIMESTAMP`), }); -// RBAC Phase 3: Audit Logging export const auditLogs = sqliteTable("audit_logs", { id: integer("id").primaryKey({ autoIncrement: true }), - // Who userId: text("user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), - username: text("username").notNull(), // Snapshot in case user deleted + username: text("username").notNull(), - // What - action: text("action").notNull(), // "create", "read", "update", "delete", "share" - resourceType: text("resource_type").notNull(), // "host", "credential", "user", "session" - resourceId: text("resource_id"), // Can be text or number, store as text - resourceName: text("resource_name"), // Human-readable identifier + action: text("action").notNull(), + resourceType: text("resource_type").notNull(), + resourceId: text("resource_id"), + resourceName: text("resource_name"), - // Context - details: text("details"), // JSON: { oldValue, newValue, reason, ... } + details: text("details"), ipAddress: text("ip_address"), userAgent: text("user_agent"), - // Result success: integer("success", { mode: "boolean" }).notNull(), errorMessage: text("error_message"), - // When timestamp: text("timestamp") .notNull() .default(sql`CURRENT_TIMESTAMP`), @@ -454,21 +432,17 @@ export const sessionRecordings = sqliteTable("session_recordings", { onDelete: "set null", }), - // Session info startedAt: text("started_at") .notNull() .default(sql`CURRENT_TIMESTAMP`), endedAt: text("ended_at"), - duration: integer("duration"), // seconds + duration: integer("duration"), - // Command log (lightweight) - commands: text("commands"), // JSON: [{ts, cmd, exitCode, blocked}] - dangerousActions: text("dangerous_actions"), // JSON: blocked commands + commands: text("commands"), + dangerousActions: text("dangerous_actions"), - // Full recording (optional, heavy) - recordingPath: text("recording_path"), // Path to .cast file + recordingPath: text("recording_path"), - // Metadata terminatedByOwner: integer("terminated_by_owner", { mode: "boolean" }) .default(false), terminationReason: text("termination_reason"), diff --git a/src/backend/database/routes/credentials.ts b/src/backend/database/routes/credentials.ts index 9d425951..6d0b0ab5 100644 --- a/src/backend/database/routes/credentials.ts +++ b/src/backend/database/routes/credentials.ts @@ -478,7 +478,6 @@ router.put( userId, ); - // Update shared credentials if this credential is shared const { SharedCredentialManager } = await import("../../utils/shared-credential-manager.js"); const sharedCredManager = SharedCredentialManager.getInstance(); @@ -541,8 +540,6 @@ router.delete( return res.status(404).json({ error: "Credential not found" }); } - // Update hosts using this credential to set credentialId to null - // This prevents orphaned references before deletion const hostsUsingCredential = await db .select() .from(sshData) @@ -570,7 +567,6 @@ router.delete( ), ); - // Revoke all shares for hosts that used this credential for (const host of hostsUsingCredential) { const revokedShares = await db .delete(hostAccess) @@ -592,16 +588,11 @@ router.delete( } } - // Delete shared credentials for this original credential - // Note: This will also be handled by CASCADE, but we do it explicitly for logging const { SharedCredentialManager } = await import("../../utils/shared-credential-manager.js"); const sharedCredManager = SharedCredentialManager.getInstance(); await sharedCredManager.deleteSharedCredentialsForOriginal(parseInt(id)); - // sshCredentialUsage will be automatically deleted by ON DELETE CASCADE - // No need for manual deletion - await db .delete(sshCredentials) .where( diff --git a/src/backend/database/routes/rbac.ts b/src/backend/database/routes/rbac.ts index 7605958a..6e6f0033 100644 --- a/src/backend/database/routes/rbac.ts +++ b/src/backend/database/routes/rbac.ts @@ -27,10 +27,8 @@ function isNonEmptyString(value: unknown): value is string { return typeof value === "string" && value.trim().length > 0; } -/** - * Share a host with a user or role - * POST /rbac/host/:id/share - */ +//Share a host with a user or role +//POST /rbac/host/:id/share router.post( "/host/:id/share", authenticateJWT, @@ -44,21 +42,19 @@ router.post( try { const { - targetType = "user", // "user" or "role" + targetType = "user", targetUserId, targetRoleId, durationHours, - permissionLevel = "view", // Only "view" is supported + permissionLevel = "view", } = req.body; - // Validate target type if (!["user", "role"].includes(targetType)) { return res .status(400) .json({ error: "Invalid target type. Must be 'user' or 'role'" }); } - // Validate required fields based on target type if (targetType === "user" && !isNonEmptyString(targetUserId)) { return res .status(400) @@ -70,7 +66,6 @@ router.post( .json({ error: "Target role ID is required when sharing with role" }); } - // Verify user owns the host const host = await db .select() .from(sshData) @@ -86,7 +81,6 @@ router.post( return res.status(403).json({ error: "Not host owner" }); } - // Check if host uses credentials (required for sharing) if (!host[0].credentialId) { return res.status(400).json({ error: @@ -95,7 +89,6 @@ router.post( }); } - // Verify target exists (user or role) if (targetType === "user") { const targetUser = await db .select({ id: users.id, username: users.username }) @@ -118,7 +111,6 @@ router.post( } } - // Calculate expiry time let expiresAt: string | null = null; if ( durationHours && @@ -130,7 +122,6 @@ router.post( expiresAt = expiryDate.toISOString(); } - // Validate permission level (only "view" is supported) const validLevels = ["view"]; if (!validLevels.includes(permissionLevel)) { return res.status(400).json({ @@ -139,7 +130,6 @@ router.post( }); } - // Check if access already exists const whereConditions = [eq(hostAccess.hostId, hostId)]; if (targetType === "user") { whereConditions.push(eq(hostAccess.userId, targetUserId)); @@ -154,7 +144,6 @@ router.post( .limit(1); if (existing.length > 0) { - // Update existing access await db .update(hostAccess) .set({ @@ -163,7 +152,6 @@ router.post( }) .where(eq(hostAccess.id, existing[0].id)); - // Re-create shared credential (delete old, create new) await db .delete(sharedCredentials) .where(eq(sharedCredentials.hostAccessId, existing[0].id)); @@ -187,16 +175,6 @@ router.post( ); } - databaseLogger.info("Updated existing host access", { - operation: "share_host", - hostId, - targetType, - targetUserId: targetType === "user" ? targetUserId : undefined, - targetRoleId: targetType === "role" ? targetRoleId : undefined, - permissionLevel, - expiresAt, - }); - return res.json({ success: true, message: "Host access updated", @@ -204,7 +182,6 @@ router.post( }); } - // Create new access const result = await db.insert(hostAccess).values({ hostId, userId: targetType === "user" ? targetUserId : null, @@ -214,7 +191,6 @@ router.post( expiresAt, }); - // Create shared credential for the target const { SharedCredentialManager } = await import("../../utils/shared-credential-manager.js"); const sharedCredManager = SharedCredentialManager.getInstance(); @@ -235,17 +211,6 @@ router.post( ); } - databaseLogger.info("Created host access", { - operation: "share_host", - hostId, - hostName: host[0].name, - targetType, - targetUserId: targetType === "user" ? targetUserId : undefined, - targetRoleId: targetType === "role" ? targetRoleId : undefined, - permissionLevel, - expiresAt, - }); - res.json({ success: true, message: `Host shared successfully with ${targetType}`, @@ -262,10 +227,8 @@ router.post( }, ); -/** - * Revoke host access - * DELETE /rbac/host/:id/access/:accessId - */ +// Revoke host access +// DELETE /rbac/host/:id/access/:accessId router.delete( "/host/:id/access/:accessId", authenticateJWT, @@ -279,7 +242,6 @@ router.delete( } try { - // Verify user owns the host const host = await db .select() .from(sshData) @@ -290,16 +252,8 @@ router.delete( return res.status(403).json({ error: "Not host owner" }); } - // Delete the access await db.delete(hostAccess).where(eq(hostAccess.id, accessId)); - databaseLogger.info("Revoked host access", { - operation: "revoke_host_access", - hostId, - accessId, - userId, - }); - res.json({ success: true, message: "Access revoked" }); } catch (error) { databaseLogger.error("Failed to revoke host access", error, { @@ -313,10 +267,8 @@ router.delete( }, ); -/** - * Get host access list - * GET /rbac/host/:id/access - */ +// Get host access list +// GET /rbac/host/:id/access router.get( "/host/:id/access", authenticateJWT, @@ -329,7 +281,6 @@ router.get( } try { - // Verify user owns the host const host = await db .select() .from(sshData) @@ -340,7 +291,6 @@ router.get( return res.status(403).json({ error: "Not host owner" }); } - // Get all access records (both user and role based) const rawAccessList = await db .select({ id: hostAccess.id, @@ -361,7 +311,6 @@ router.get( .where(eq(hostAccess.hostId, hostId)) .orderBy(desc(hostAccess.createdAt)); - // Format access list with type information const accessList = rawAccessList.map((access) => ({ id: access.id, targetType: access.userId ? "user" : "role", @@ -389,10 +338,8 @@ router.get( }, ); -/** - * Get user's shared hosts (hosts shared WITH this user) - * GET /rbac/shared-hosts - */ +// Get user's shared hosts (hosts shared WITH this user) +// GET /rbac/shared-hosts router.get( "/shared-hosts", authenticateJWT, @@ -438,10 +385,8 @@ router.get( }, ); -/** - * Get all roles - * GET /rbac/roles - */ +// Get all roles +// GET /rbac/roles router.get( "/roles", authenticateJWT, @@ -468,14 +413,8 @@ router.get( }, ); -// ============================================================================ -// Role Management (CRUD) -// ============================================================================ - -/** - * Get all roles - * GET /rbac/roles - */ +// Get all roles +// GET /rbac/roles router.get( "/roles", authenticateJWT, @@ -504,10 +443,8 @@ router.get( }, ); -/** - * Create new role - * POST /rbac/roles - */ +// Create new role +// POST /rbac/roles router.post( "/roles", authenticateJWT, @@ -515,14 +452,12 @@ router.post( async (req: AuthenticatedRequest, res: Response) => { const { name, displayName, description } = req.body; - // Validate required fields if (!isNonEmptyString(name) || !isNonEmptyString(displayName)) { return res.status(400).json({ error: "Role name and display name are required", }); } - // Validate name format (alphanumeric, underscore, hyphen only) if (!/^[a-z0-9_-]+$/.test(name)) { return res.status(400).json({ error: @@ -531,7 +466,6 @@ router.post( } try { - // Check if role name already exists const existing = await db .select({ id: roles.id }) .from(roles) @@ -544,23 +478,16 @@ router.post( }); } - // Create new role const result = await db.insert(roles).values({ name, displayName, description: description || null, isSystem: false, - permissions: null, // Roles are for grouping only + permissions: null, }); const newRoleId = result.lastInsertRowid; - databaseLogger.info("Created new role", { - operation: "create_role", - roleId: newRoleId, - roleName: name, - }); - res.status(201).json({ success: true, roleId: newRoleId, @@ -576,10 +503,8 @@ router.post( }, ); -/** - * Update role - * PUT /rbac/roles/:id - */ +// Update role +// PUT /rbac/roles/:id router.put( "/roles/:id", authenticateJWT, @@ -592,7 +517,6 @@ router.put( return res.status(400).json({ error: "Invalid role ID" }); } - // Validate at least one field to update if (!displayName && description === undefined) { return res.status(400).json({ error: "At least one field (displayName or description) is required", @@ -600,7 +524,6 @@ router.put( } try { - // Get existing role const existingRole = await db .select({ id: roles.id, @@ -615,7 +538,6 @@ router.put( return res.status(404).json({ error: "Role not found" }); } - // Build update object const updates: { displayName?: string; description?: string | null; @@ -632,15 +554,8 @@ router.put( updates.description = description || null; } - // Update role await db.update(roles).set(updates).where(eq(roles.id, roleId)); - databaseLogger.info("Updated role", { - operation: "update_role", - roleId, - roleName: existingRole[0].name, - }); - res.json({ success: true, message: "Role updated successfully", @@ -655,10 +570,8 @@ router.put( }, ); -/** - * Delete role - * DELETE /rbac/roles/:id - */ +// Delete role +// DELETE /rbac/roles/:id router.delete( "/roles/:id", authenticateJWT, @@ -671,7 +584,6 @@ router.delete( } try { - // Get role details const role = await db .select({ id: roles.id, @@ -686,41 +598,28 @@ router.delete( return res.status(404).json({ error: "Role not found" }); } - // Cannot delete system roles if (role[0].isSystem) { return res.status(403).json({ error: "Cannot delete system roles", }); } - // Delete user-role assignments first const deletedUserRoles = await db .delete(userRoles) .where(eq(userRoles.roleId, roleId)) .returning({ userId: userRoles.userId }); - // Invalidate permission cache for affected users for (const { userId } of deletedUserRoles) { permissionManager.invalidateUserPermissionCache(userId); } - // Delete host_access entries for this role const deletedHostAccess = await db .delete(hostAccess) .where(eq(hostAccess.roleId, roleId)) .returning({ id: hostAccess.id }); - // Note: sharedCredentials will be auto-deleted by CASCADE - - // Delete role await db.delete(roles).where(eq(roles.id, roleId)); - databaseLogger.info("Deleted role", { - operation: "delete_role", - roleId, - roleName: role[0].name, - }); - res.json({ success: true, message: "Role deleted successfully", @@ -735,14 +634,8 @@ router.delete( }, ); -// ============================================================================ -// User-Role Assignment -// ============================================================================ - -/** - * Assign role to user - * POST /rbac/users/:userId/roles - */ +// Assign role to user +// POST /rbac/users/:userId/roles router.post( "/users/:userId/roles", authenticateJWT, @@ -758,7 +651,6 @@ router.post( return res.status(400).json({ error: "Role ID is required" }); } - // Verify target user exists const targetUser = await db .select() .from(users) @@ -769,7 +661,6 @@ router.post( return res.status(404).json({ error: "User not found" }); } - // Verify role exists const role = await db .select() .from(roles) @@ -780,7 +671,6 @@ router.post( return res.status(404).json({ error: "Role not found" }); } - // Prevent manual assignment of system roles if (role[0].isSystem) { return res.status(403).json({ error: @@ -788,7 +678,6 @@ router.post( }); } - // Check if already assigned const existing = await db .select() .from(userRoles) @@ -801,14 +690,12 @@ router.post( return res.status(409).json({ error: "Role already assigned" }); } - // Assign role await db.insert(userRoles).values({ userId: targetUserId, roleId, grantedBy: currentUserId, }); - // Create shared credentials for all hosts shared with this role const hostsSharedWithRole = await db .select() .from(hostAccess) @@ -839,31 +726,12 @@ router.post( hostId: ssh_data.id, }, ); - // Continue with other hosts even if one fails } } } - if (hostsSharedWithRole.length > 0) { - databaseLogger.info("Created shared credentials for new role member", { - operation: "assign_role_create_credentials", - targetUserId, - roleId, - hostCount: hostsSharedWithRole.length, - }); - } - - // Invalidate permission cache permissionManager.invalidateUserPermissionCache(targetUserId); - databaseLogger.info("Assigned role to user", { - operation: "assign_role", - targetUserId, - roleId, - roleName: role[0].name, - grantedBy: currentUserId, - }); - res.json({ success: true, message: "Role assigned successfully", @@ -878,10 +746,8 @@ router.post( }, ); -/** - * Remove role from user - * DELETE /rbac/users/:userId/roles/:roleId - */ +// Remove role from user +// DELETE /rbac/users/:userId/roles/:roleId router.delete( "/users/:userId/roles/:roleId", authenticateJWT, @@ -895,7 +761,6 @@ router.delete( } try { - // Verify role exists and get its details const role = await db .select({ id: roles.id, @@ -910,7 +775,6 @@ router.delete( return res.status(404).json({ error: "Role not found" }); } - // Prevent removal of system roles if (role[0].isSystem) { return res.status(403).json({ error: @@ -918,22 +782,14 @@ router.delete( }); } - // Delete the user-role assignment await db .delete(userRoles) .where( and(eq(userRoles.userId, targetUserId), eq(userRoles.roleId, roleId)), ); - // Invalidate permission cache permissionManager.invalidateUserPermissionCache(targetUserId); - databaseLogger.info("Removed role from user", { - operation: "remove_role", - targetUserId, - roleId, - }); - res.json({ success: true, message: "Role removed successfully", @@ -949,10 +805,8 @@ router.delete( }, ); -/** - * Get user's roles - * GET /rbac/users/:userId/roles - */ +// Get user's roles +// GET /rbac/users/:userId/roles router.get( "/users/:userId/roles", authenticateJWT, @@ -960,7 +814,6 @@ router.get( const targetUserId = req.params.userId; const currentUserId = req.userId!; - // Users can only see their own roles unless they're admin if ( targetUserId !== currentUserId && !(await permissionManager.isAdmin(currentUserId)) diff --git a/src/backend/database/routes/ssh.ts b/src/backend/database/routes/ssh.ts index ff32ca8c..6e7086c7 100644 --- a/src/backend/database/routes/ssh.ts +++ b/src/backend/database/routes/ssh.ts @@ -604,7 +604,6 @@ router.put( } try { - // Check if user can update this host (owner or manage permission) const accessInfo = await permissionManager.canAccessHost( userId, Number(hostId), @@ -620,7 +619,6 @@ router.put( return res.status(403).json({ error: "Access denied" }); } - // Shared users cannot edit hosts (view-only) if (!accessInfo.isOwner) { sshLogger.warn("Shared user attempted to update host (view-only)", { operation: "host_update", @@ -632,7 +630,6 @@ router.put( }); } - // Get the actual owner ID for the update const hostRecord = await db .select({ userId: sshData.userId, @@ -654,7 +651,6 @@ router.put( const ownerId = hostRecord[0].userId; - // Only owner can change credentialId if ( !accessInfo.isOwner && sshDataObj.credentialId !== undefined && @@ -665,7 +661,6 @@ router.put( }); } - // Only owner can change authType if ( !accessInfo.isOwner && sshDataObj.authType !== undefined && @@ -676,31 +671,15 @@ router.put( }); } - // Check if credentialId is changing from non-null to null - // This happens when switching from "credential" auth to "password"/"key"/"none" if (sshDataObj.credentialId !== undefined) { if ( hostRecord[0].credentialId !== null && sshDataObj.credentialId === null ) { - // Auth type changed away from credential - revoke all shares const revokedShares = await db .delete(hostAccess) .where(eq(hostAccess.hostId, Number(hostId))) .returning({ id: hostAccess.id, userId: hostAccess.userId }); - - if (revokedShares.length > 0) { - sshLogger.info( - "Auto-revoked host shares due to auth type change from credential", - { - operation: "auto_revoke_shares", - hostId: Number(hostId), - revokedCount: revokedShares.length, - reason: "auth_type_changed_from_credential", - }, - ); - // Note: sharedCredentials will be auto-deleted by CASCADE - } } } @@ -830,17 +809,14 @@ router.get( try { const now = new Date().toISOString(); - // Get user's role IDs const userRoleIds = await db .select({ roleId: userRoles.roleId }) .from(userRoles) .where(eq(userRoles.userId, userId)); const roleIds = userRoleIds.map((r) => r.roleId); - // Query own hosts + shared hosts with access check const rawData = await db .select({ - // All ssh_data fields id: sshData.id, userId: sshData.userId, name: sshData.name, @@ -881,7 +857,6 @@ router.get( socks5Password: sshData.socks5Password, socks5ProxyChain: sshData.socks5ProxyChain, - // Shared access info ownerId: sshData.userId, isShared: sql`${hostAccess.id} IS NOT NULL`, permissionLevel: hostAccess.permissionLevel, @@ -903,15 +878,13 @@ router.get( ) .where( or( - eq(sshData.userId, userId), // Own hosts + eq(sshData.userId, userId), and( - // Shared to user directly (not expired) eq(hostAccess.userId, userId), or(isNull(hostAccess.expiresAt), gte(hostAccess.expiresAt, now)), ), roleIds.length > 0 ? and( - // Shared to user's role (not expired) inArray(hostAccess.roleId, roleIds), or( isNull(hostAccess.expiresAt), @@ -922,11 +895,9 @@ router.get( ), ); - // Separate own hosts from shared hosts for proper decryption const ownHosts = rawData.filter((row) => row.userId === userId); const sharedHosts = rawData.filter((row) => row.userId !== userId); - // Decrypt own hosts with user's DEK let decryptedOwnHosts: any[] = []; try { decryptedOwnHosts = await SimpleDBOps.select( @@ -934,38 +905,16 @@ router.get( "ssh_data", userId, ); - sshLogger.debug("Own hosts decrypted successfully", { - operation: "host_fetch_own_decrypted", - userId, - count: decryptedOwnHosts.length, - }); } catch (decryptError) { sshLogger.error("Failed to decrypt own hosts", decryptError, { operation: "host_fetch_own_decrypt_failed", userId, }); - // Return empty array if decryption fails decryptedOwnHosts = []; } - // For shared hosts, DON'T try to decrypt them with user's DEK - // Just pass them through as plain objects without encrypted credential fields - // The credentials will be resolved via SharedCredentialManager later when resolveHostCredentials is called - sshLogger.info("Processing shared hosts", { - operation: "host_fetch_shared_process", - userId, - count: sharedHosts.length, - }); - const sanitizedSharedHosts = sharedHosts; - sshLogger.info("Combining hosts", { - operation: "host_fetch_combine", - userId, - ownCount: decryptedOwnHosts.length, - sharedCount: sanitizedSharedHosts.length, - }); - const data = [...decryptedOwnHosts, ...sanitizedSharedHosts]; const result = await Promise.all( @@ -1001,7 +950,6 @@ router.get( ? JSON.parse(row.socks5ProxyChain as string) : [], - // Add shared access metadata isShared: !!row.isShared, permissionLevel: row.permissionLevel || undefined, sharedExpiresAt: row.expiresAt || undefined, @@ -1013,12 +961,6 @@ router.get( }), ); - sshLogger.info("Credential resolution complete, sending response", { - operation: "host_fetch_complete", - userId, - hostCount: result.length, - }); - res.json(result); } catch (err) { sshLogger.error("Failed to fetch SSH hosts from database", err, { @@ -1220,7 +1162,6 @@ router.delete( const numericHostId = Number(hostId); - // Delete all related data in correct order (child tables first) await db .delete(fileManagerRecent) .where(eq(fileManagerRecent.hostId, numericHostId)); @@ -1245,15 +1186,12 @@ router.delete( .delete(recentActivity) .where(eq(recentActivity.hostId, numericHostId)); - // Delete RBAC host access entries await db.delete(hostAccess).where(eq(hostAccess.hostId, numericHostId)); - // Delete session recordings await db .delete(sessionRecordings) .where(eq(sessionRecordings.hostId, numericHostId)); - // Finally delete the host itself await db .delete(sshData) .where(and(eq(sshData.id, numericHostId), eq(sshData.userId, userId))); @@ -1762,21 +1700,11 @@ async function resolveHostCredentials( requestingUserId?: string, ): Promise> { try { - sshLogger.info("Resolving credentials for host", { - operation: "resolve_credentials_start", - hostId: host.id as number, - hasCredentialId: !!host.credentialId, - requestingUserId, - ownerId: (host.ownerId || host.userId) as string, - }); - if (host.credentialId && (host.userId || host.ownerId)) { const credentialId = host.credentialId as number; const ownerId = (host.ownerId || host.userId) as string; - // Check if this is a shared host access if (requestingUserId && requestingUserId !== ownerId) { - // User is accessing a shared host - use shared credential try { const { SharedCredentialManager } = await import("../../utils/shared-credential-manager.js"); @@ -1796,7 +1724,6 @@ async function resolveHostCredentials( keyType: sharedCred.keyType, }; - // Only override username if overrideCredentialUsername is not enabled if (!host.overrideCredentialUsername) { resolvedHost.username = sharedCred.username; } @@ -1816,11 +1743,9 @@ async function resolveHostCredentials( : "Unknown error", }, ); - // Fall through to try owner's credential } } - // Original owner access - use original credential const credentials = await SimpleDBOps.select( db .select() @@ -1846,7 +1771,6 @@ async function resolveHostCredentials( keyType: credential.key_type || credential.keyType, }; - // Only override username if overrideCredentialUsername is not enabled if (!host.overrideCredentialUsername) { resolvedHost.username = credential.username; } @@ -2053,7 +1977,6 @@ router.delete( const hostIds = hostsToDelete.map((host) => host.id); - // Delete all related data for all hosts in the folder (child tables first) if (hostIds.length > 0) { await db .delete(fileManagerRecent) @@ -2079,21 +2002,17 @@ router.delete( .delete(recentActivity) .where(inArray(recentActivity.hostId, hostIds)); - // Delete RBAC host access entries await db.delete(hostAccess).where(inArray(hostAccess.hostId, hostIds)); - // Delete session recordings await db .delete(sessionRecordings) .where(inArray(sessionRecordings.hostId, hostIds)); } - // Now delete the hosts themselves await db .delete(sshData) .where(and(eq(sshData.userId, userId), eq(sshData.folder, folderName))); - // Finally delete the folder metadata await db .delete(sshFolders) .where( diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index f00224d0..7d896ca7 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -139,33 +139,12 @@ function isNonEmptyString(val: unknown): val is string { const authenticateJWT = authManager.createAuthMiddleware(); const requireAdmin = authManager.createAdminMiddleware(); -/** - * Comprehensive user deletion utility that ensures all related data is deleted - * in proper order to avoid foreign key constraint errors. - * - * This function explicitly deletes all user-related data before deleting the user record. - * It wraps everything in a transaction for atomicity. - * - * @param userId - The ID of the user to delete - * @returns Promise - * @throws Error if deletion fails - */ async function deleteUserAndRelatedData(userId: string): Promise { try { - authLogger.info("Starting comprehensive user data deletion", { - operation: "delete_user_and_related_data_start", - userId, - }); - - // Delete all related data in proper order to avoid FK constraint errors - // Order matters due to foreign key relationships - - // 1. Delete credential usage logs await db .delete(sshCredentialUsage) .where(eq(sshCredentialUsage.userId, userId)); - // 2. Delete file manager data await db .delete(fileManagerRecent) .where(eq(fileManagerRecent.userId, userId)); @@ -176,32 +155,23 @@ async function deleteUserAndRelatedData(userId: string): Promise { .delete(fileManagerShortcuts) .where(eq(fileManagerShortcuts.userId, userId)); - // 3. Delete activity and alerts await db.delete(recentActivity).where(eq(recentActivity.userId, userId)); await db.delete(dismissedAlerts).where(eq(dismissedAlerts.userId, userId)); - // 4. Delete snippets and snippet folders await db.delete(snippets).where(eq(snippets.userId, userId)); await db.delete(snippetFolders).where(eq(snippetFolders.userId, userId)); - // 5. Delete SSH folders await db.delete(sshFolders).where(eq(sshFolders.userId, userId)); - // 6. Delete command history await db.delete(commandHistory).where(eq(commandHistory.userId, userId)); - // 7. Delete SSH data and credentials await db.delete(sshData).where(eq(sshData.userId, userId)); await db.delete(sshCredentials).where(eq(sshCredentials.userId, userId)); - // 8. Delete user-specific settings (encryption keys, etc.) db.$client .prepare("DELETE FROM settings WHERE key LIKE ?") .run(`user_%_${userId}`); - // 9. Finally, delete the user record - // Note: Sessions, user_roles, host_access, audit_logs, and session_recordings - // will be automatically deleted via CASCADE DELETE foreign key constraints await db.delete(users).where(eq(users.id, userId)); authLogger.success("User and all related data deleted successfully", { @@ -293,7 +263,6 @@ router.post("/create", async (req, res) => { totp_backup_codes: null, }); - // Assign default role to new user try { const defaultRoleName = isFirstUser ? "admin" : "user"; const defaultRole = await db @@ -306,12 +275,7 @@ router.post("/create", async (req, res) => { await db.insert(userRoles).values({ userId: id, roleId: defaultRole[0].id, - grantedBy: id, // Self-assigned during registration - }); - authLogger.info("Assigned default role to new user", { - operation: "assign_default_role", - userId: id, - roleName: defaultRoleName, + grantedBy: id, }); } else { authLogger.warn("Default role not found during user registration", { @@ -325,7 +289,6 @@ router.post("/create", async (req, res) => { operation: "assign_default_role", userId: id, }); - // Don't fail user creation if role assignment fails } try { @@ -934,7 +897,6 @@ router.get("/oidc/callback", async (req, res) => { scopes: String(config.scopes), }); - // Assign default role to new OIDC user try { const defaultRoleName = isFirstUser ? "admin" : "user"; const defaultRole = await db @@ -947,12 +909,7 @@ router.get("/oidc/callback", async (req, res) => { await db.insert(userRoles).values({ userId: id, roleId: defaultRole[0].id, - grantedBy: id, // Self-assigned during registration - }); - authLogger.info("Assigned default role to new OIDC user", { - operation: "assign_default_role_oidc", - userId: id, - roleName: defaultRoleName, + grantedBy: id, }); } else { authLogger.warn( @@ -973,7 +930,6 @@ router.get("/oidc/callback", async (req, res) => { userId: id, }, ); - // Don't fail user creation if role assignment fails } try { @@ -1215,7 +1171,6 @@ router.post("/login", async (req, res) => { return res.status(401).json({ error: "Incorrect password" }); } - // Re-encrypt any pending shared credentials for this user try { const { SharedCredentialManager } = await import("../../utils/shared-credential-manager.js"); @@ -1227,7 +1182,6 @@ router.post("/login", async (req, res) => { userId: userRecord.id, error, }); - // Continue with login even if re-encryption fails } if (userRecord.totp_enabled) { @@ -1303,15 +1257,7 @@ router.post("/logout", authenticateJWT, async (req, res) => { try { const payload = await authManager.verifyJWTToken(token); sessionId = payload?.sessionId; - } catch (error) { - authLogger.debug( - "Token verification failed during logout (expected if token expired)", - { - operation: "logout_token_verify_failed", - userId, - }, - ); - } + } catch (error) {} } await authManager.logoutUser(userId, sessionId); @@ -2840,11 +2786,9 @@ router.post("/link-oidc-to-password", authenticateJWT, async (req, res) => { }); } - // Revoke all sessions and logout the OIDC user before deletion await authManager.revokeAllUserSessions(oidcUserId); authManager.logoutUser(oidcUserId); - // Use the comprehensive deletion utility to ensure all data is properly deleted await deleteUserAndRelatedData(oidcUserId); try { diff --git a/src/backend/ssh/docker-console.ts b/src/backend/ssh/docker-console.ts index 45b3eab1..1ee99ecb 100644 --- a/src/backend/ssh/docker-console.ts +++ b/src/backend/ssh/docker-console.ts @@ -21,7 +21,6 @@ interface SSHSession { const activeSessions = new Map(); -// WebSocket server on port 30008 const wss = new WebSocketServer({ port: 30008, verifyClient: async (info, callback) => { @@ -49,14 +48,8 @@ const wss = new WebSocketServer({ return callback(false, 401, "Invalid token"); } - // Store userId in the request for later use (info.req as any).userId = decoded.userId; - dockerConsoleLogger.info("WebSocket connection verified", { - operation: "ws_verify", - userId: decoded.userId, - }); - callback(true); } catch (error) { dockerConsoleLogger.error("WebSocket verification error", error, { @@ -67,7 +60,6 @@ const wss = new WebSocketServer({ }, }); -// Helper function to detect available shell in container async function detectShell( session: SSHSession, containerId: string, @@ -102,19 +94,15 @@ async function detectShell( ); }); - // If we get here, the shell was found return shell; } catch { - // Try next shell continue; } } - // Default to sh if nothing else works return "sh"; } -// Helper function to create jump host chain async function createJumpHostChain( jumpHosts: any[], userId: string, @@ -128,7 +116,6 @@ async function createJumpHostChain( for (let i = 0; i < jumpHosts.length; i++) { const jumpHostId = jumpHosts[i].hostId; - // Fetch jump host from database const jumpHostData = await SimpleDBOps.select( getDb() .select() @@ -154,7 +141,6 @@ async function createJumpHostChain( } } - // Resolve credentials for jump host let resolvedCredentials: any = { password: jumpHost.password, sshKey: jumpHost.key, @@ -203,7 +189,6 @@ async function createJumpHostChain( tcpKeepAliveInitialDelay: 30000, }; - // Set authentication if ( resolvedCredentials.authType === "password" && resolvedCredentials.password @@ -223,7 +208,6 @@ async function createJumpHostChain( } } - // If we have a previous client, use it as the sock if (currentClient) { await new Promise((resolve, reject) => { currentClient!.forwardOut( @@ -252,17 +236,10 @@ async function createJumpHostChain( return currentClient; } -// Handle WebSocket connections wss.on("connection", async (ws: WebSocket, req) => { const userId = (req as any).userId; const sessionId = `docker-console-${Date.now()}-${Math.random()}`; - dockerConsoleLogger.info("Docker console WebSocket connected", { - operation: "ws_connect", - sessionId, - userId, - }); - let sshSession: SSHSession | null = null; ws.on("message", async (data) => { @@ -304,7 +281,6 @@ wss.on("connection", async (ws: WebSocket, req) => { return; } - // Check if Docker is enabled for this host if (!hostConfig.enableDocker) { ws.send( JSON.stringify({ @@ -317,7 +293,6 @@ wss.on("connection", async (ws: WebSocket, req) => { } try { - // Resolve credentials let resolvedCredentials: any = { password: hostConfig.password, sshKey: hostConfig.key, @@ -355,7 +330,6 @@ wss.on("connection", async (ws: WebSocket, req) => { } } - // Create SSH client const client = new SSHClient(); const config: any = { @@ -370,7 +344,6 @@ wss.on("connection", async (ws: WebSocket, req) => { tcpKeepAliveInitialDelay: 30000, }; - // Set authentication if ( resolvedCredentials.authType === "password" && resolvedCredentials.password @@ -390,7 +363,6 @@ wss.on("connection", async (ws: WebSocket, req) => { } } - // Handle jump hosts if configured if (hostConfig.jumpHosts && hostConfig.jumpHosts.length > 0) { const jumpClient = await createJumpHostChain( hostConfig.jumpHosts, @@ -413,7 +385,6 @@ wss.on("connection", async (ws: WebSocket, req) => { } } - // Connect to SSH await new Promise((resolve, reject) => { client.on("ready", () => resolve()); client.on("error", reject); @@ -429,10 +400,8 @@ wss.on("connection", async (ws: WebSocket, req) => { activeSessions.set(sessionId, sshSession); - // Validate or detect shell let shellToUse = shell || "bash"; - // If a shell is explicitly provided, verify it exists in the container if (shell) { try { await new Promise((resolve, reject) => { @@ -461,7 +430,6 @@ wss.on("connection", async (ws: WebSocket, req) => { ); }); } catch { - // Requested shell not found, detect available shell dockerConsoleLogger.warn( `Requested shell ${shell} not found, detecting available shell`, { @@ -474,13 +442,11 @@ wss.on("connection", async (ws: WebSocket, req) => { shellToUse = await detectShell(sshSession, containerId); } } else { - // No shell specified, detect available shell shellToUse = await detectShell(sshSession, containerId); } sshSession.shell = shellToUse; - // Create docker exec PTY const execCommand = `docker exec -it ${containerId} /bin/${shellToUse}`; client.exec( @@ -515,7 +481,6 @@ wss.on("connection", async (ws: WebSocket, req) => { sshSession!.stream = stream; - // Forward stream output to WebSocket stream.on("data", (data: Buffer) => { if (ws.readyState === WebSocket.OPEN) { ws.send( @@ -527,15 +492,7 @@ wss.on("connection", async (ws: WebSocket, req) => { } }); - stream.stderr.on("data", (data: Buffer) => { - // Log stderr but don't send to terminal to avoid duplicate error messages - dockerConsoleLogger.debug("Docker exec stderr", { - operation: "docker_exec_stderr", - sessionId, - containerId, - data: data.toString("utf8"), - }); - }); + stream.stderr.on("data", (data: Buffer) => {}); stream.on("close", () => { if (ws.readyState === WebSocket.OPEN) { @@ -547,7 +504,6 @@ wss.on("connection", async (ws: WebSocket, req) => { ); } - // Cleanup if (sshSession) { sshSession.client.end(); activeSessions.delete(sessionId); @@ -564,14 +520,6 @@ wss.on("connection", async (ws: WebSocket, req) => { }, }), ); - - dockerConsoleLogger.info("Docker console session started", { - operation: "console_start", - sessionId, - containerId, - shell: shellToUse, - requestedShell: shell, - }); }, ); } catch (error) { @@ -605,13 +553,6 @@ wss.on("connection", async (ws: WebSocket, req) => { if (sshSession && sshSession.stream) { const { cols, rows } = message.data; sshSession.stream.setWindow(rows, cols); - - dockerConsoleLogger.debug("Console resized", { - operation: "console_resize", - sessionId, - cols, - rows, - }); } break; } @@ -624,11 +565,6 @@ wss.on("connection", async (ws: WebSocket, req) => { sshSession.client.end(); activeSessions.delete(sessionId); - dockerConsoleLogger.info("Docker console disconnected", { - operation: "console_disconnect", - sessionId, - }); - ws.send( JSON.stringify({ type: "disconnected", @@ -640,7 +576,6 @@ wss.on("connection", async (ws: WebSocket, req) => { } case "ping": { - // Respond with pong to acknowledge keepalive if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: "pong" })); } @@ -669,12 +604,6 @@ wss.on("connection", async (ws: WebSocket, req) => { }); ws.on("close", () => { - dockerConsoleLogger.info("WebSocket connection closed", { - operation: "ws_close", - sessionId, - }); - - // Cleanup SSH session if still active if (sshSession) { if (sshSession.stream) { sshSession.stream.end(); @@ -690,7 +619,6 @@ wss.on("connection", async (ws: WebSocket, req) => { sessionId, }); - // Cleanup if (sshSession) { if (sshSession.stream) { sshSession.stream.end(); @@ -701,37 +629,17 @@ wss.on("connection", async (ws: WebSocket, req) => { }); }); -dockerConsoleLogger.info( - "Docker console WebSocket server started on port 30008", - { - operation: "startup", - }, -); - -// Graceful shutdown process.on("SIGTERM", () => { - dockerConsoleLogger.info("Shutting down Docker console server...", { - operation: "shutdown", - }); - - // Close all active sessions activeSessions.forEach((session, sessionId) => { if (session.stream) { session.stream.end(); } session.client.end(); - dockerConsoleLogger.info("Closed session during shutdown", { - operation: "shutdown", - sessionId, - }); }); activeSessions.clear(); wss.close(() => { - dockerConsoleLogger.info("Docker console server closed", { - operation: "shutdown", - }); process.exit(0); }); }); diff --git a/src/backend/ssh/docker.ts b/src/backend/ssh/docker.ts index a2181677..923f8f71 100644 --- a/src/backend/ssh/docker.ts +++ b/src/backend/ssh/docker.ts @@ -11,10 +11,8 @@ import { SimpleDBOps } from "../utils/simple-db-ops.js"; import { AuthManager } from "../utils/auth-manager.js"; import type { AuthenticatedRequest, SSHHost } from "../../types/index.js"; -// Create dedicated logger for Docker operations const dockerLogger = logger; -// SSH Session Management interface SSHSession { client: SSHClient; isConnected: boolean; @@ -26,7 +24,6 @@ interface SSHSession { const sshSessions: Record = {}; -// Session cleanup with 60-minute idle timeout const SESSION_IDLE_TIMEOUT = 60 * 60 * 1000; function cleanupSession(sessionId: string) { @@ -47,9 +44,7 @@ function cleanupSession(sessionId: string) { try { session.client.end(); - } catch (error) { - dockerLogger.debug("Error ending SSH client during cleanup", { error }); - } + } catch (error) {} clearTimeout(session.timeout); delete sshSessions[sessionId]; dockerLogger.info("Docker SSH session cleaned up", { @@ -70,7 +65,6 @@ function scheduleSessionCleanup(sessionId: string) { } } -// Helper function to resolve jump host async function resolveJumpHost( hostId: number, userId: string, @@ -131,7 +125,6 @@ async function resolveJumpHost( } } -// Helper function to create jump host chain async function createJumpHostChain( jumpHosts: Array<{ hostId: number }>, userId: string, @@ -239,7 +232,6 @@ async function createJumpHostChain( } } -// Helper function to execute Docker CLI commands async function executeDockerCommand( session: SSHSession, command: string, @@ -290,7 +282,6 @@ async function executeDockerCommand( }); } -// Express app setup const app = express(); app.use( @@ -334,12 +325,9 @@ app.use(cookieParser()); app.use(express.json({ limit: "100mb" })); app.use(express.urlencoded({ limit: "100mb", extended: true })); -// Initialize AuthManager and apply middleware const authManager = AuthManager.getInstance(); app.use(authManager.createAuthMiddleware()); -// Session management endpoints - // POST /docker/ssh/connect - Establish SSH session app.post("/docker/ssh/connect", async (req, res) => { const { sessionId, hostId } = req.body; @@ -373,7 +361,6 @@ app.post("/docker/ssh/connect", async (req, res) => { } try { - // Get host configuration - check both owned and shared hosts const hosts = await SimpleDBOps.select( getDb().select().from(sshData).where(eq(sshData.id, hostId)), "ssh_data", @@ -386,7 +373,6 @@ app.post("/docker/ssh/connect", async (req, res) => { const host = hosts[0] as unknown as SSHHost; - // Verify user has access to this host (either owner or shared access) if (host.userId !== userId) { const { PermissionManager } = await import("../utils/permission-manager.js"); @@ -417,7 +403,6 @@ app.post("/docker/ssh/connect", async (req, res) => { } } - // Check if Docker is enabled for this host if (!host.enableDocker) { dockerLogger.warn("Docker not enabled for host", { operation: "docker_connect", @@ -431,12 +416,10 @@ app.post("/docker/ssh/connect", async (req, res) => { }); } - // Clean up existing session if any if (sshSessions[sessionId]) { cleanupSession(sessionId); } - // Resolve credentials let resolvedCredentials: any = { password: host.password, sshKey: host.key, @@ -447,9 +430,7 @@ app.post("/docker/ssh/connect", async (req, res) => { if (host.credentialId) { const ownerId = host.userId; - // Check if this is a shared host access if (userId !== ownerId) { - // User is accessing a shared host - use shared credential try { const { SharedCredentialManager } = await import("../utils/shared-credential-manager.js"); @@ -475,7 +456,6 @@ app.post("/docker/ssh/connect", async (req, res) => { }); } } else { - // Owner accessing their own host const credentials = await SimpleDBOps.select( getDb() .select() @@ -503,7 +483,6 @@ app.post("/docker/ssh/connect", async (req, res) => { } } - // Create SSH client const client = new SSHClient(); const config: any = { @@ -518,7 +497,6 @@ app.post("/docker/ssh/connect", async (req, res) => { tcpKeepAliveInitialDelay: 30000, }; - // Set authentication if ( resolvedCredentials.authType === "password" && resolvedCredentials.password @@ -554,13 +532,6 @@ app.post("/docker/ssh/connect", async (req, res) => { scheduleSessionCleanup(sessionId); - dockerLogger.info("Docker SSH session established", { - operation: "docker_connect", - sessionId, - hostId, - userId, - }); - res.json({ success: true, message: "SSH connection established" }); }); @@ -588,7 +559,6 @@ app.post("/docker/ssh/connect", async (req, res) => { } }); - // Handle jump hosts if configured if (host.jumpHosts && host.jumpHosts.length > 0) { const jumpClient = await createJumpHostChain( host.jumpHosts as Array<{ hostId: number }>, @@ -654,11 +624,6 @@ app.post("/docker/ssh/disconnect", async (req, res) => { cleanupSession(sessionId); - dockerLogger.info("Docker SSH session disconnected", { - operation: "docker_disconnect", - sessionId, - }); - res.json({ success: true, message: "SSH session disconnected" }); }); @@ -724,7 +689,6 @@ app.get("/docker/validate/:sessionId", async (req, res) => { session.activeOperations++; try { - // Check if Docker is installed try { const versionOutput = await executeDockerCommand( session, @@ -733,7 +697,6 @@ app.get("/docker/validate/:sessionId", async (req, res) => { const versionMatch = versionOutput.match(/Docker version ([^\s,]+)/); const version = versionMatch ? versionMatch[1] : "unknown"; - // Check if Docker daemon is running try { await executeDockerCommand(session, "docker ps >/dev/null 2>&1"); @@ -798,7 +761,7 @@ app.get("/docker/validate/:sessionId", async (req, res) => { // GET /docker/containers/:sessionId - List all containers app.get("/docker/containers/:sessionId", async (req, res) => { const { sessionId } = req.params; - const all = req.query.all !== "false"; // Default to true + const all = req.query.all !== "false"; const userId = (req as any).userId; if (!userId) { @@ -942,13 +905,6 @@ app.post( session.activeOperations--; - dockerLogger.info("Container started", { - operation: "start_container", - sessionId, - containerId, - userId, - }); - res.json({ success: true, message: "Container started successfully", @@ -1007,13 +963,6 @@ app.post( session.activeOperations--; - dockerLogger.info("Container stopped", { - operation: "stop_container", - sessionId, - containerId, - userId, - }); - res.json({ success: true, message: "Container stopped successfully", @@ -1072,13 +1021,6 @@ app.post( session.activeOperations--; - dockerLogger.info("Container restarted", { - operation: "restart_container", - sessionId, - containerId, - userId, - }); - res.json({ success: true, message: "Container restarted successfully", @@ -1137,13 +1079,6 @@ app.post( session.activeOperations--; - dockerLogger.info("Container paused", { - operation: "pause_container", - sessionId, - containerId, - userId, - }); - res.json({ success: true, message: "Container paused successfully", @@ -1202,13 +1137,6 @@ app.post( session.activeOperations--; - dockerLogger.info("Container unpaused", { - operation: "unpause_container", - sessionId, - containerId, - userId, - }); - res.json({ success: true, message: "Container unpaused successfully", @@ -1272,14 +1200,6 @@ app.delete( session.activeOperations--; - dockerLogger.info("Container removed", { - operation: "remove_container", - sessionId, - containerId, - force, - userId, - }); - res.json({ success: true, message: "Container removed successfully", @@ -1425,17 +1345,14 @@ app.get( const output = await executeDockerCommand(session, command); const rawStats = JSON.parse(output.trim()); - // Parse memory usage (e.g., "1.5GiB / 8GiB" -> { used: "1.5GiB", limit: "8GiB" }) const memoryParts = rawStats.memory.split(" / "); const memoryUsed = memoryParts[0]?.trim() || "0B"; const memoryLimit = memoryParts[1]?.trim() || "0B"; - // Parse network I/O (e.g., "1.5MB / 2.3MB" -> { input: "1.5MB", output: "2.3MB" }) const netIOParts = rawStats.netIO.split(" / "); const netInput = netIOParts[0]?.trim() || "0B"; const netOutput = netIOParts[1]?.trim() || "0B"; - // Parse block I/O (e.g., "10MB / 5MB" -> { read: "10MB", write: "5MB" }) const blockIOParts = rawStats.blockIO.split(" / "); const blockRead = blockIOParts[0]?.trim() || "0B"; const blockWrite = blockIOParts[1]?.trim() || "0B"; @@ -1482,13 +1399,11 @@ app.get( }, ); -// Start server const PORT = 30007; app.listen(PORT, async () => { try { await authManager.initialize(); - dockerLogger.info(`Docker backend server started on port ${PORT}`); } catch (err) { dockerLogger.error("Failed to initialize Docker backend", err, { operation: "startup", @@ -1496,9 +1411,7 @@ app.listen(PORT, async () => { } }); -// Graceful shutdown process.on("SIGINT", () => { - dockerLogger.info("Shutting down Docker backend"); Object.keys(sshSessions).forEach((sessionId) => { cleanupSession(sessionId); }); @@ -1506,7 +1419,6 @@ process.on("SIGINT", () => { }); process.on("SIGTERM", () => { - dockerLogger.info("Shutting down Docker backend"); Object.keys(sshSessions).forEach((sessionId) => { cleanupSession(sessionId); }); diff --git a/src/backend/ssh/file-manager.ts b/src/backend/ssh/file-manager.ts index 10eae574..939acb21 100644 --- a/src/backend/ssh/file-manager.ts +++ b/src/backend/ssh/file-manager.ts @@ -815,34 +815,10 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { }, ); - fileLogger.info("SFTP connection request received", { - operation: "sftp_connect_request", - sessionId, - hostId, - ip, - port, - useSocks5, - socks5Host, - socks5Port, - hasSocks5ProxyChain: !!( - socks5ProxyChain && (socks5ProxyChain as any).length > 0 - ), - proxyChainLength: socks5ProxyChain ? (socks5ProxyChain as any).length : 0, - }); - - // Check if SOCKS5 proxy is enabled (either single proxy or chain) if ( useSocks5 && (socks5Host || (socks5ProxyChain && (socks5ProxyChain as any).length > 0)) ) { - fileLogger.info("SOCKS5 enabled for SFTP, creating connection", { - operation: "sftp_socks5_enabled", - sessionId, - socks5Host, - socks5Port, - hasChain: !!(socks5ProxyChain && (socks5ProxyChain as any).length > 0), - }); - try { const socks5Socket = await createSocks5Connection(ip, port, { useSocks5, @@ -854,10 +830,6 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { }); if (socks5Socket) { - fileLogger.info("SOCKS5 socket created for SFTP", { - operation: "sftp_socks5_socket_ready", - sessionId, - }); config.sock = socks5Socket; client.connect(config); return; @@ -883,17 +855,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { : "Unknown error"), }); } - } else { - fileLogger.info("SOCKS5 NOT enabled for SFTP connection", { - operation: "sftp_no_socks5", - sessionId, - useSocks5, - socks5Host, - hasChain: !!(socks5ProxyChain && (socks5ProxyChain as any).length > 0), - }); - } - - if (jumpHosts && jumpHosts.length > 0 && userId) { + } else if (jumpHosts && jumpHosts.length > 0 && userId) { try { const jumpClient = await createJumpHostChain(jumpHosts, userId); @@ -976,9 +938,7 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => { delete pendingTOTPSessions[sessionId]; try { session.client.end(); - } catch (error) { - sshLogger.debug("Operation failed, continuing", { error }); - } + } catch (error) {} fileLogger.warn("TOTP session timeout before code submission", { operation: "file_totp_verify", sessionId, @@ -3055,21 +3015,10 @@ app.post("/ssh/file_manager/ssh/extractArchive", async (req, res) => { let errorOutput = ""; - stream.on("data", (data: Buffer) => { - fileLogger.debug("Extract stdout", { - operation: "extract_archive", - sessionId, - output: data.toString(), - }); - }); + stream.on("data", (data: Buffer) => {}); stream.stderr.on("data", (data: Buffer) => { errorOutput += data.toString(); - fileLogger.debug("Extract stderr", { - operation: "extract_archive", - sessionId, - error: data.toString(), - }); }); stream.on("close", (code: number) => { @@ -3247,21 +3196,10 @@ app.post("/ssh/file_manager/ssh/compressFiles", async (req, res) => { let errorOutput = ""; - stream.on("data", (data: Buffer) => { - fileLogger.debug("Compress stdout", { - operation: "compress_files", - sessionId, - output: data.toString(), - }); - }); + stream.on("data", (data: Buffer) => {}); stream.stderr.on("data", (data: Buffer) => { errorOutput += data.toString(); - fileLogger.debug("Compress stderr", { - operation: "compress_files", - sessionId, - error: data.toString(), - }); }); stream.on("close", (code: number) => { diff --git a/src/backend/ssh/server-stats.ts b/src/backend/ssh/server-stats.ts index 2a65c76a..9bbc3b86 100644 --- a/src/backend/ssh/server-stats.ts +++ b/src/backend/ssh/server-stats.ts @@ -201,7 +201,6 @@ class SSHConnectionPool { private cleanupInterval: NodeJS.Timeout; constructor() { - // Reduce cleanup interval from 5 minutes to 2 minutes for faster dead connection removal this.cleanupInterval = setInterval( () => { this.cleanup(); @@ -211,8 +210,6 @@ class SSHConnectionPool { } private getHostKey(host: SSHHostWithCredentials): string { - // Include SOCKS5 settings in the key to ensure separate connection pools - // for direct connections vs SOCKS5 connections const socks5Key = host.useSocks5 ? `:socks5:${host.socks5Host}:${host.socks5Port}:${JSON.stringify(host.socks5ProxyChain || [])}` : ""; @@ -221,9 +218,8 @@ class SSHConnectionPool { private isConnectionHealthy(client: Client): boolean { try { - // Check if the connection has been destroyed or closed - // @ts-ignore - accessing internal property to check connection state - if (client._sock && (client._sock.destroyed || !client._sock.writable)) { + const sock = (client as any)._sock; + if (sock && (sock.destroyed || !sock.writable)) { return false; } return true; @@ -236,28 +232,13 @@ class SSHConnectionPool { const hostKey = this.getHostKey(host); let connections = this.connections.get(hostKey) || []; - statsLogger.info("Getting connection from pool", { - operation: "get_connection_from_pool", - hostKey: hostKey, - availableConnections: connections.length, - useSocks5: host.useSocks5, - socks5Host: host.socks5Host, - hasSocks5ProxyChain: !!( - host.socks5ProxyChain && host.socks5ProxyChain.length > 0 - ), - hostId: host.id, - }); - - // Find available connection and validate health const available = connections.find((conn) => !conn.inUse); if (available) { - // Health check before reuse if (!this.isConnectionHealthy(available.client)) { statsLogger.warn("Removing unhealthy connection from pool", { operation: "remove_dead_connection", hostKey, }); - // Remove dead connection try { available.client.end(); } catch (error) { @@ -265,12 +246,7 @@ class SSHConnectionPool { } connections = connections.filter((c) => c !== available); this.connections.set(hostKey, connections); - // Fall through to create new connection } else { - statsLogger.info("Reusing existing connection from pool", { - operation: "reuse_connection", - hostKey, - }); available.inUse = true; available.lastUsed = Date.now(); return available.client; @@ -278,10 +254,6 @@ class SSHConnectionPool { } if (connections.length < this.maxConnectionsPerHost) { - statsLogger.info("Creating new connection for pool", { - operation: "create_new_connection", - hostKey, - }); const client = await this.createConnection(host); const pooled: PooledConnection = { client, @@ -369,24 +341,11 @@ class SSHConnectionPool { try { const config = buildSshConfig(host); - // Check if SOCKS5 proxy is enabled (either single proxy or chain) if ( host.useSocks5 && (host.socks5Host || (host.socks5ProxyChain && host.socks5ProxyChain.length > 0)) ) { - statsLogger.info("Using SOCKS5 proxy for connection", { - operation: "socks5_enabled", - hostIp: host.ip, - hostPort: host.port, - socks5Host: host.socks5Host, - socks5Port: host.socks5Port, - hasChain: !!( - host.socks5ProxyChain && host.socks5ProxyChain.length > 0 - ), - chainLength: host.socks5ProxyChain?.length || 0, - }); - try { const socks5Socket = await createSocks5Connection( host.ip, @@ -402,10 +361,6 @@ class SSHConnectionPool { ); if (socks5Socket) { - statsLogger.info("SOCKS5 socket created successfully", { - operation: "socks5_socket_ready", - hostIp: host.ip, - }); config.sock = socks5Socket; client.connect(config); return; @@ -492,12 +447,6 @@ class SSHConnectionPool { const hostKey = this.getHostKey(host); const connections = this.connections.get(hostKey) || []; - statsLogger.info("Clearing all connections for host", { - operation: "clear_host_connections", - hostKey, - connectionCount: connections.length, - }); - for (const conn of connections) { try { conn.client.end(); @@ -519,7 +468,6 @@ class SSHConnectionPool { for (const [hostKey, connections] of this.connections.entries()) { const activeConnections = connections.filter((conn) => { - // Remove if idle for too long if (!conn.inUse && now - conn.lastUsed > maxAge) { try { conn.client.end(); @@ -527,7 +475,6 @@ class SSHConnectionPool { totalCleaned++; return false; } - // Also remove if connection is unhealthy (even if recently used) if (!this.isConnectionHealthy(conn.client)) { statsLogger.warn("Removing unhealthy connection during cleanup", { operation: "cleanup_unhealthy", @@ -549,23 +496,9 @@ class SSHConnectionPool { this.connections.set(hostKey, activeConnections); } } - - if (totalCleaned > 0 || totalUnhealthy > 0) { - statsLogger.info("Connection pool cleanup completed", { - operation: "cleanup_complete", - idleCleaned: totalCleaned, - unhealthyCleaned: totalUnhealthy, - remainingHosts: this.connections.size, - }); - } } clearAllConnections(): void { - statsLogger.info("Clearing ALL connections from pool", { - operation: "clear_all_connections", - totalHosts: this.connections.size, - }); - for (const [hostKey, connections] of this.connections.entries()) { for (const conn of connections) { try { @@ -601,13 +534,12 @@ class SSHConnectionPool { class RequestQueue { private queues = new Map Promise>>(); private processing = new Set(); - private requestTimeout = 60000; // 60 second timeout for requests + private requestTimeout = 60000; async queueRequest(hostId: number, request: () => Promise): Promise { return new Promise((resolve, reject) => { const wrappedRequest = async () => { try { - // Add timeout wrapper to prevent indefinite hanging const result = await Promise.race([ request(), new Promise((_, rej) => @@ -646,19 +578,11 @@ class RequestQueue { if (request) { try { await request(); - } catch (error) { - // Log errors but continue processing queue - statsLogger.debug("Request queue error", { - operation: "queue_request_error", - hostId, - error: error instanceof Error ? error.message : String(error), - }); - } + } catch (error) {} } } this.processing.delete(hostId); - // Check if new items were added during processing const currentQueue = this.queues.get(hostId); if (currentQueue && currentQueue.length > 0) { this.processQueue(hostId); @@ -797,9 +721,9 @@ class AuthFailureTracker { class PollingBackoff { private failures = new Map(); - private baseDelay = 30000; // 30s base delay - private maxDelay = 600000; // 10 min max delay - private maxRetries = 5; // Max retry attempts before giving up + private baseDelay = 30000; + private maxDelay = 600000; + private maxRetries = 5; recordFailure(hostId: number): void { const existing = this.failures.get(hostId) || { count: 0, nextRetry: 0 }; @@ -811,25 +735,16 @@ class PollingBackoff { count: existing.count + 1, nextRetry: Date.now() + delay, }); - - statsLogger.debug("Recorded polling backoff", { - operation: "polling_backoff_recorded", - hostId, - failureCount: existing.count + 1, - nextRetryDelay: delay, - }); } shouldSkip(hostId: number): boolean { const backoff = this.failures.get(hostId); if (!backoff) return false; - // If exceeded max retries, always skip if (backoff.count >= this.maxRetries) { return true; } - // Otherwise check if we're still in backoff period return Date.now() < backoff.nextRetry; } @@ -852,18 +767,13 @@ class PollingBackoff { reset(hostId: number): void { this.failures.delete(hostId); - statsLogger.debug("Reset polling backoff", { - operation: "polling_backoff_reset", - hostId, - }); } cleanup(): void { - const maxAge = 60 * 60 * 1000; // 1 hour + const maxAge = 60 * 60 * 1000; const now = Date.now(); for (const [hostId, backoff] of this.failures.entries()) { - // Only cleanup if not at max retries and old enough if (backoff.count < this.maxRetries && now - backoff.nextRetry > maxAge) { this.failures.delete(hostId); } @@ -906,7 +816,6 @@ interface SSHHostWithCredentials { updatedAt: string; userId: string; - // SOCKS5 Proxy configuration useSocks5?: boolean; socks5Host?: string; socks5Port?: number; @@ -1051,7 +960,6 @@ class PollingManager { } private async pollHostStatus(host: SSHHostWithCredentials): Promise { - // Refresh host data from database to get latest settings const refreshedHost = await fetchHostById(host.id, host.userId); if (!refreshedHost) { statsLogger.warn("Host not found during status polling", { @@ -1082,18 +990,11 @@ class PollingManager { } private async pollHostMetrics(host: SSHHostWithCredentials): Promise { - // Check if we should skip due to backoff if (pollingBackoff.shouldSkip(host.id)) { const backoffInfo = pollingBackoff.getBackoffInfo(host.id); - statsLogger.debug("Skipping metrics polling due to backoff", { - operation: "poll_metrics_skipped", - hostId: host.id, - backoffInfo, - }); return; } - // Refresh host data from database to get latest SOCKS5 and other settings const refreshedHost = await fetchHostById(host.id, host.userId); if (!refreshedHost) { statsLogger.warn("Host not found during metrics polling", { @@ -1114,13 +1015,11 @@ class PollingManager { data: metrics, timestamp: Date.now(), }); - // Reset backoff on successful collection pollingBackoff.reset(refreshedHost.id); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - // Record failure for backoff pollingBackoff.recordFailure(refreshedHost.id); const latestConfig = this.pollingConfigs.get(refreshedHost.id); @@ -1356,7 +1255,6 @@ async function resolveHostCredentials( createdAt: host.createdAt, updatedAt: host.updatedAt, userId: host.userId, - // SOCKS5 proxy settings useSocks5: !!host.useSocks5, socks5Host: host.socks5Host || undefined, socks5Port: host.socks5Port || undefined, @@ -1415,21 +1313,6 @@ async function resolveHostCredentials( addLegacyCredentials(baseHost, host); } - statsLogger.info("Resolved host credentials with SOCKS5 settings", { - operation: "resolve_host", - hostId: host.id as number, - useSocks5: baseHost.useSocks5, - socks5Host: baseHost.socks5Host, - socks5Port: baseHost.socks5Port, - hasSocks5ProxyChain: !!( - baseHost.socks5ProxyChain && - (baseHost.socks5ProxyChain as any[]).length > 0 - ), - proxyChainLength: baseHost.socks5ProxyChain - ? (baseHost.socks5ProxyChain as any[]).length - : 0, - }); - return baseHost as unknown as SSHHostWithCredentials; } catch (error) { statsLogger.error( @@ -1654,12 +1537,7 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{ }; try { login_stats = await collectLoginStats(client); - } catch (e) { - statsLogger.debug("Failed to collect login stats", { - operation: "login_stats_failed", - error: e instanceof Error ? e.message : String(e), - }); - } + } catch (e) {} const result = { cpu, @@ -1800,7 +1678,6 @@ app.post("/refresh", async (req, res) => { }); } - // Clear all connections to ensure fresh connections with updated settings connectionPool.clearAllConnections(); await pollingManager.refreshHostPolling(userId); @@ -1825,7 +1702,6 @@ app.post("/host-updated", async (req, res) => { try { const host = await fetchHostById(hostId, userId); if (host) { - // Clear existing connections for this host to ensure new settings (like SOCKS5) are used connectionPool.clearHostConnections(host); await pollingManager.startPollingForHost(host); diff --git a/src/backend/ssh/terminal.ts b/src/backend/ssh/terminal.ts index a908fcda..83f622b2 100644 --- a/src/backend/ssh/terminal.ts +++ b/src/backend/ssh/terminal.ts @@ -137,12 +137,10 @@ async function createJumpHostChain( const clients: Client[] = []; try { - // Fetch all jump host configurations in parallel const jumpHostConfigs = await Promise.all( jumpHosts.map((jh) => resolveJumpHost(jh.hostId, userId)), ); - // Validate all configs resolved for (let i = 0; i < jumpHostConfigs.length; i++) { if (!jumpHostConfigs[i]) { sshLogger.error(`Jump host ${i + 1} not found`, undefined, { @@ -154,7 +152,6 @@ async function createJumpHostChain( } } - // Connect through jump hosts sequentially for (let i = 0; i < jumpHostConfigs.length; i++) { const jumpHostConfig = jumpHostConfigs[i]; @@ -1196,7 +1193,6 @@ wss.on("connection", async (ws: WebSocket, req) => { return; } - // Check if SOCKS5 proxy is enabled (either single proxy or chain) if ( hostConfig.useSocks5 && (hostConfig.socks5Host || diff --git a/src/backend/ssh/tunnel.ts b/src/backend/ssh/tunnel.ts index 373a129a..70e06747 100644 --- a/src/backend/ssh/tunnel.ts +++ b/src/backend/ssh/tunnel.ts @@ -594,11 +594,6 @@ async function connectSSHTunnel( keyType: sharedCred.keyType, authMethod: sharedCred.authType, }; - tunnelLogger.info("Resolved shared credentials for tunnel source", { - operation: "tunnel_connect_shared_cred", - tunnelName, - userId: effectiveUserId, - }); } else { const errorMessage = `Cannot connect tunnel '${tunnelName}': shared credentials not available`; tunnelLogger.error(errorMessage); @@ -1126,7 +1121,6 @@ async function connectSSHTunnel( }); } - // Check if SOCKS5 proxy is enabled (either single proxy or chain) if ( tunnelConfig.useSocks5 && (tunnelConfig.socks5Host || @@ -1399,7 +1393,6 @@ async function killRemoteTunnelByMarker( callback(err); }); - // Check if SOCKS5 proxy is enabled (either single proxy or chain) if ( tunnelConfig.useSocks5 && (tunnelConfig.socks5Host || @@ -1517,12 +1510,6 @@ app.post( if (accessInfo.isShared && !accessInfo.isOwner) { tunnelConfig.requestingUserId = userId; - tunnelLogger.info("Shared host tunnel connect", { - operation: "tunnel_connect_shared", - userId, - hostId: tunnelConfig.sourceHostId, - tunnelName, - }); } } @@ -1552,14 +1539,7 @@ app.post( } } - // If endpoint details are missing, resolve them from database if (!tunnelConfig.endpointIP || !tunnelConfig.endpointUsername) { - tunnelLogger.info("Resolving endpoint host details from database", { - operation: "tunnel_connect_resolve_endpoint", - tunnelName, - endpointHost: tunnelConfig.endpointHost, - }); - try { const systemCrypto = SystemCrypto.getInstance(); const internalAuthToken = await systemCrypto.getInternalAuthToken(); @@ -1587,7 +1567,6 @@ app.post( ); } - // Populate endpoint fields tunnelConfig.endpointIP = endpointHost.ip; tunnelConfig.endpointSSHPort = endpointHost.port; tunnelConfig.endpointUsername = endpointHost.username; @@ -1598,13 +1577,6 @@ app.post( tunnelConfig.endpointKeyType = endpointHost.keyType; tunnelConfig.endpointCredentialId = endpointHost.credentialId; tunnelConfig.endpointUserId = endpointHost.userId; - - tunnelLogger.info("Endpoint host details resolved", { - operation: "tunnel_connect_endpoint_resolved", - tunnelName, - endpointIP: tunnelConfig.endpointIP, - endpointUsername: tunnelConfig.endpointUsername, - }); } catch (resolveError) { tunnelLogger.error( "Failed to resolve endpoint host", diff --git a/src/backend/ssh/widgets/cpu-collector.ts b/src/backend/ssh/widgets/cpu-collector.ts index 5beb022a..90eb579b 100644 --- a/src/backend/ssh/widgets/cpu-collector.ts +++ b/src/backend/ssh/widgets/cpu-collector.ts @@ -26,7 +26,6 @@ export async function collectCpuMetrics(client: Client): Promise<{ let loadTriplet: [number, number, number] | null = null; try { - // Wrap Promise.all with timeout to prevent indefinite blocking const [stat1, loadAvgOut, coresOut] = await Promise.race([ Promise.all([ execCommand(client, "cat /proc/stat"), diff --git a/src/backend/utils/auth-manager.ts b/src/backend/utils/auth-manager.ts index 1c2d9b52..10dd5662 100644 --- a/src/backend/utils/auth-manager.ts +++ b/src/backend/utils/auth-manager.ts @@ -169,7 +169,6 @@ class AuthManager { await saveMemoryDatabaseToFile(); } - // Migrate credentials to system encryption for offline sharing try { const { CredentialSystemEncryptionMigration } = await import("./credential-system-encryption-migration.js"); @@ -177,18 +176,9 @@ class AuthManager { const credResult = await credMigration.migrateUserCredentials(userId); if (credResult.migrated > 0) { - databaseLogger.info( - "Credentials migrated to system encryption on login", - { - operation: "login_credential_migration", - userId, - migrated: credResult.migrated, - }, - ); await saveMemoryDatabaseToFile(); } } catch (error) { - // Log but don't fail login databaseLogger.warn("Credential migration failed during login", { operation: "login_credential_migration_failed", userId, diff --git a/src/backend/utils/credential-system-encryption-migration.ts b/src/backend/utils/credential-system-encryption-migration.ts index e8f430ae..ffabd66a 100644 --- a/src/backend/utils/credential-system-encryption-migration.ts +++ b/src/backend/utils/credential-system-encryption-migration.ts @@ -6,31 +6,21 @@ import { SystemCrypto } from "./system-crypto.js"; import { FieldCrypto } from "./field-crypto.js"; import { databaseLogger } from "./logger.js"; -/** - * Migrates credentials to include system-encrypted fields for offline sharing - */ export class CredentialSystemEncryptionMigration { - /** - * Migrates a user's credentials to include system-encrypted fields - * Requires user to be logged in (DEK available) - */ async migrateUserCredentials(userId: string): Promise<{ migrated: number; failed: number; skipped: number; }> { try { - // Get user's DEK (requires logged in) const userDEK = DataCrypto.getUserDataKey(userId); if (!userDEK) { throw new Error("User must be logged in to migrate credentials"); } - // Get system key const systemCrypto = SystemCrypto.getInstance(); const CSKEK = await systemCrypto.getCredentialSharingKey(); - // Find credentials without system encryption const credentials = await db .select() .from(sshCredentials) @@ -51,7 +41,6 @@ export class CredentialSystemEncryptionMigration { for (const cred of credentials) { try { - // Decrypt with user DEK const plainPassword = cred.password ? FieldCrypto.decryptField( cred.password, @@ -79,7 +68,6 @@ export class CredentialSystemEncryptionMigration { ) : null; - // Re-encrypt with CSKEK const systemPassword = plainPassword ? FieldCrypto.encryptField( plainPassword, @@ -107,7 +95,6 @@ export class CredentialSystemEncryptionMigration { ) : null; - // Update database await db .update(sshCredentials) .set({ @@ -119,12 +106,6 @@ export class CredentialSystemEncryptionMigration { .where(eq(sshCredentials.id, cred.id)); migrated++; - - databaseLogger.info("Credential migrated for offline sharing", { - operation: "credential_system_encryption_migrated", - credentialId: cred.id, - userId, - }); } catch (error) { databaseLogger.error("Failed to migrate credential", error, { credentialId: cred.id, @@ -133,20 +114,6 @@ export class CredentialSystemEncryptionMigration { failed++; } } - - if (migrated > 0) { - databaseLogger.success( - "Credential system encryption migration completed", - { - operation: "credential_migration_complete", - userId, - migrated, - failed, - skipped, - }, - ); - } - return { migrated, failed, skipped }; } catch (error) { databaseLogger.error( diff --git a/src/backend/utils/data-crypto.ts b/src/backend/utils/data-crypto.ts index 19f0326f..4513b62c 100644 --- a/src/backend/utils/data-crypto.ts +++ b/src/backend/utils/data-crypto.ts @@ -488,12 +488,10 @@ class DataCrypto { const systemEncrypted: Record = {}; const recordId = record.id || "temp-" + Date.now(); - // Only encrypt for sshCredentials table if (tableName !== "ssh_credentials") { return systemEncrypted as Partial; } - // Encrypt password field if (record.password && typeof record.password === "string") { systemEncrypted.systemPassword = FieldCrypto.encryptField( record.password as string, @@ -503,7 +501,6 @@ class DataCrypto { ); } - // Encrypt key field if (record.key && typeof record.key === "string") { systemEncrypted.systemKey = FieldCrypto.encryptField( record.key as string, @@ -513,7 +510,6 @@ class DataCrypto { ); } - // Encrypt key_password field if (record.key_password && typeof record.key_password === "string") { systemEncrypted.systemKeyPassword = FieldCrypto.encryptField( record.key_password as string, diff --git a/src/backend/utils/database-file-encryption.ts b/src/backend/utils/database-file-encryption.ts index f0adc96a..8ace6c46 100644 --- a/src/backend/utils/database-file-encryption.ts +++ b/src/backend/utils/database-file-encryption.ts @@ -327,11 +327,7 @@ class DatabaseFileEncryption { fs.accessSync(envPath, fs.constants.R_OK); envFileReadable = true; } - } catch (error) { - databaseLogger.debug("Operation failed, continuing", { - error: error instanceof Error ? error.message : String(error), - }); - } + } catch (error) {} databaseLogger.error( "Database decryption authentication failed - possible causes: wrong DATABASE_KEY, corrupted files, or interrupted write", diff --git a/src/backend/utils/permission-manager.ts b/src/backend/utils/permission-manager.ts index 38dc8549..fdaafb2b 100644 --- a/src/backend/utils/permission-manager.ts +++ b/src/backend/utils/permission-manager.ts @@ -19,7 +19,7 @@ interface HostAccessInfo { hasAccess: boolean; isOwner: boolean; isShared: boolean; - permissionLevel?: "view"; // Only "view" is supported for shared access + permissionLevel?: "view"; expiresAt?: string | null; } @@ -34,12 +34,11 @@ class PermissionManager { string, { permissions: string[]; timestamp: number } >; - private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes + private readonly CACHE_TTL = 5 * 60 * 1000; private constructor() { this.permissionCache = new Map(); - // Auto-cleanup expired host access every 1 minute setInterval(() => { this.cleanupExpiredAccess().catch((error) => { databaseLogger.error( @@ -52,7 +51,6 @@ class PermissionManager { }); }, 60 * 1000); - // Clear permission cache every 5 minutes setInterval(() => { this.clearPermissionCache(); }, this.CACHE_TTL); @@ -80,13 +78,6 @@ class PermissionManager { ), ) .returning({ id: hostAccess.id }); - - if (result.length > 0) { - databaseLogger.info("Cleaned up expired host access", { - operation: "host_access_cleanup", - count: result.length, - }); - } } catch (error) { databaseLogger.error("Failed to cleanup expired host access", error, { operation: "host_access_cleanup_failed", @@ -112,7 +103,6 @@ class PermissionManager { * Get user permissions from roles */ async getUserPermissions(userId: string): Promise { - // Check cache first const cached = this.permissionCache.get(userId); if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) { return cached.permissions; @@ -145,7 +135,6 @@ class PermissionManager { const permissionsArray = Array.from(allPermissions); - // Cache the result this.permissionCache.set(userId, { permissions: permissionsArray, timestamp: Date.now(), @@ -168,17 +157,14 @@ class PermissionManager { async hasPermission(userId: string, permission: string): Promise { const userPermissions = await this.getUserPermissions(userId); - // Check for wildcard "*" (god mode) if (userPermissions.includes("*")) { return true; } - // Check exact match if (userPermissions.includes(permission)) { return true; } - // Check wildcard matches const parts = permission.split("."); for (let i = parts.length; i > 0; i--) { const wildcardPermission = parts.slice(0, i).join(".") + ".*"; @@ -199,7 +185,6 @@ class PermissionManager { action: "read" | "write" | "execute" | "delete" | "share" = "read", ): Promise { try { - // Check if user is the owner const host = await db .select() .from(sshData) @@ -214,14 +199,12 @@ class PermissionManager { }; } - // Get user's role IDs const userRoleIds = await db .select({ roleId: userRoles.roleId }) .from(userRoles) .where(eq(userRoles.userId, userId)); const roleIds = userRoleIds.map((r) => r.roleId); - // Check if host is shared with user OR user's roles const now = new Date().toISOString(); const sharedAccess = await db .select() @@ -246,7 +229,6 @@ class PermissionManager { if (sharedAccess.length > 0) { const access = sharedAccess[0]; - // All shared access is view-only - deny write/delete if (action === "write" || action === "delete") { return { hasAccess: false, @@ -257,7 +239,6 @@ class PermissionManager { }; } - // Update last accessed time try { await db .update(hostAccess) @@ -306,7 +287,6 @@ class PermissionManager { */ async isAdmin(userId: string): Promise { try { - // Check old is_admin field const user = await db .select({ isAdmin: users.is_admin }) .from(users) @@ -317,7 +297,6 @@ class PermissionManager { return true; } - // Check if user has admin or super_admin role const adminRoles = await db .select({ roleName: roles.name }) .from(userRoles) @@ -415,7 +394,6 @@ class PermissionManager { }); } - // Attach access info to request for use in route handlers (req as any).hostAccessInfo = accessInfo; next(); diff --git a/src/backend/utils/shared-credential-manager.ts b/src/backend/utils/shared-credential-manager.ts index 57687c34..8fc0114a 100644 --- a/src/backend/utils/shared-credential-manager.ts +++ b/src/backend/utils/shared-credential-manager.ts @@ -49,14 +49,11 @@ class SharedCredentialManager { ownerId: string, ): Promise { try { - // Try owner's DEK first (existing path) const ownerDEK = DataCrypto.getUserDataKey(ownerId); if (ownerDEK) { - // Owner online - use existing flow const targetDEK = DataCrypto.getUserDataKey(targetUserId); if (!targetDEK) { - // Target user is offline, mark for lazy re-encryption await this.createPendingSharedCredential( hostAccessId, originalCredentialId, @@ -65,14 +62,12 @@ class SharedCredentialManager { return; } - // Fetch and decrypt original credential using owner's DEK const credentialData = await this.getDecryptedCredential( originalCredentialId, ownerId, ownerDEK, ); - // Encrypt credential data with target user's DEK const encryptedForTarget = this.encryptCredentialForUser( credentialData, targetUserId, @@ -80,7 +75,6 @@ class SharedCredentialManager { hostAccessId, ); - // Store shared credential await db.insert(sharedCredentials).values({ hostAccessId, originalCredentialId, @@ -88,28 +82,9 @@ class SharedCredentialManager { ...encryptedForTarget, needsReEncryption: false, }); - - databaseLogger.info("Created shared credential for user", { - operation: "create_shared_credential", - hostAccessId, - targetUserId, - }); } else { - // NEW: Owner offline - use system key fallback - databaseLogger.info( - "Owner offline, attempting to share using system key", - { - operation: "create_shared_credential_system_key", - hostAccessId, - targetUserId, - ownerId, - }, - ); - - // Get target user's DEK const targetDEK = DataCrypto.getUserDataKey(targetUserId); if (!targetDEK) { - // Both offline - create pending await this.createPendingSharedCredential( hostAccessId, originalCredentialId, @@ -118,11 +93,9 @@ class SharedCredentialManager { return; } - // Decrypt using system key const credentialData = await this.getDecryptedCredentialViaSystemKey(originalCredentialId); - // Encrypt for target user const encryptedForTarget = this.encryptCredentialForUser( credentialData, targetUserId, @@ -130,7 +103,6 @@ class SharedCredentialManager { hostAccessId, ); - // Store shared credential await db.insert(sharedCredentials).values({ hostAccessId, originalCredentialId, @@ -138,12 +110,6 @@ class SharedCredentialManager { ...encryptedForTarget, needsReEncryption: false, }); - - databaseLogger.info("Created shared credential using system key", { - operation: "create_shared_credential_system_key", - hostAccessId, - targetUserId, - }); } } catch (error) { databaseLogger.error("Failed to create shared credential", error, { @@ -166,13 +132,11 @@ class SharedCredentialManager { ownerId: string, ): Promise { try { - // Get all users in the role const roleUsers = await db .select({ userId: userRoles.userId }) .from(userRoles) .where(eq(userRoles.roleId, roleId)); - // Create shared credential for each user for (const { userId } of roleUsers) { try { await this.createSharedCredentialForUser( @@ -192,16 +156,8 @@ class SharedCredentialManager { userId, }, ); - // Continue with other users even if one fails } } - - databaseLogger.info("Created shared credentials for role", { - operation: "create_shared_credentials_role", - hostAccessId, - roleId, - userCount: roleUsers.length, - }); } catch (error) { databaseLogger.error( "Failed to create shared credentials for role", @@ -230,7 +186,6 @@ class SharedCredentialManager { throw new Error(`User ${userId} data not unlocked`); } - // Find shared credential via hostAccess const sharedCred = await db .select() .from(sharedCredentials) @@ -252,7 +207,6 @@ class SharedCredentialManager { const cred = sharedCred[0].shared_credentials; - // Check if needs re-encryption if (cred.needsReEncryption) { databaseLogger.warn( "Shared credential needs re-encryption but cannot be accessed yet", @@ -262,12 +216,9 @@ class SharedCredentialManager { userId, }, ); - // Credential is pending re-encryption - owner must be offline - // Return null instead of trying to re-encrypt (which would cause infinite loop) return null; } - // Decrypt credential data with user's DEK return this.decryptSharedCredential(cred, userDEK); } catch (error) { databaseLogger.error("Failed to get shared credential", error, { @@ -288,34 +239,21 @@ class SharedCredentialManager { ownerId: string, ): Promise { try { - // Get all shared credentials for this original credential const sharedCreds = await db .select() .from(sharedCredentials) .where(eq(sharedCredentials.originalCredentialId, credentialId)); - // Try owner's DEK first const ownerDEK = DataCrypto.getUserDataKey(ownerId); let credentialData: CredentialData; if (ownerDEK) { - // Owner online - use owner's DEK credentialData = await this.getDecryptedCredential( credentialId, ownerId, ownerDEK, ); } else { - // Owner offline - use system key fallback - databaseLogger.info( - "Updating shared credentials using system key (owner offline)", - { - operation: "update_shared_credentials_system_key", - credentialId, - ownerId, - }, - ); - try { credentialData = await this.getDecryptedCredentialViaSystemKey(credentialId); @@ -329,7 +267,6 @@ class SharedCredentialManager { error: error instanceof Error ? error.message : "Unknown error", }, ); - // Mark all shared credentials for re-encryption await db .update(sharedCredentials) .set({ needsReEncryption: true }) @@ -338,12 +275,10 @@ class SharedCredentialManager { } } - // Update each shared credential for (const sharedCred of sharedCreds) { const targetDEK = DataCrypto.getUserDataKey(sharedCred.targetUserId); if (!targetDEK) { - // Target user offline, mark for lazy re-encryption await db .update(sharedCredentials) .set({ needsReEncryption: true }) @@ -351,7 +286,6 @@ class SharedCredentialManager { continue; } - // Re-encrypt with target user's DEK const encryptedForTarget = this.encryptCredentialForUser( credentialData, sharedCred.targetUserId, @@ -368,12 +302,6 @@ class SharedCredentialManager { }) .where(eq(sharedCredentials.id, sharedCred.id)); } - - databaseLogger.info("Updated shared credentials for original", { - operation: "update_shared_credentials", - credentialId, - count: sharedCreds.length, - }); } catch (error) { databaseLogger.error("Failed to update shared credentials", error, { operation: "update_shared_credentials", @@ -394,12 +322,6 @@ class SharedCredentialManager { .delete(sharedCredentials) .where(eq(sharedCredentials.originalCredentialId, credentialId)) .returning({ id: sharedCredentials.id }); - - databaseLogger.info("Deleted shared credentials for original", { - operation: "delete_shared_credentials", - credentialId, - count: result.length, - }); } catch (error) { databaseLogger.error("Failed to delete shared credentials", error, { operation: "delete_shared_credentials", @@ -416,7 +338,7 @@ class SharedCredentialManager { try { const userDEK = DataCrypto.getUserDataKey(userId); if (!userDEK) { - return; // User not unlocked yet + return; } const pendingCreds = await db @@ -432,14 +354,6 @@ class SharedCredentialManager { for (const cred of pendingCreds) { await this.reEncryptSharedCredential(cred.id, userId); } - - if (pendingCreds.length > 0) { - databaseLogger.info("Re-encrypted pending credentials for user", { - operation: "reencrypt_pending_credentials", - userId, - count: pendingCreds.length, - }); - } } catch (error) { databaseLogger.error("Failed to re-encrypt pending credentials", error, { operation: "reencrypt_pending_credentials", @@ -448,8 +362,6 @@ class SharedCredentialManager { } } - // ========== PRIVATE HELPER METHODS ========== - private async getDecryptedCredential( credentialId: number, ownerId: string, @@ -472,8 +384,6 @@ class SharedCredentialManager { const cred = creds[0]; - // Decrypt sensitive fields - // Note: username and authType are NOT encrypted return { username: cred.username, authType: cred.authType, @@ -513,7 +423,6 @@ class SharedCredentialManager { const cred = creds[0]; - // Check if system fields exist if (!cred.systemPassword && !cred.systemKey && !cred.systemKeyPassword) { throw new Error( "Credential not yet migrated for offline sharing. " + @@ -521,12 +430,10 @@ class SharedCredentialManager { ); } - // Get system key const { SystemCrypto } = await import("./system-crypto.js"); const systemCrypto = SystemCrypto.getInstance(); const CSKEK = await systemCrypto.getCredentialSharingKey(); - // Decrypt using system-encrypted fields return { username: cred.username, authType: cred.authType, @@ -575,7 +482,7 @@ class SharedCredentialManager { recordId, "username", ), - encryptedAuthType: credentialData.authType, // authType is not sensitive + encryptedAuthType: credentialData.authType, encryptedPassword: credentialData.password ? FieldCrypto.encryptField( credentialData.password, @@ -660,7 +567,6 @@ class SharedCredentialManager { fieldName, ); } catch (error) { - // If decryption fails, value might not be encrypted (legacy data) databaseLogger.warn("Field decryption failed, returning as-is", { operation: "decrypt_field", fieldName, @@ -675,12 +581,11 @@ class SharedCredentialManager { originalCredentialId: number, targetUserId: string, ): Promise { - // Create placeholder with needsReEncryption flag await db.insert(sharedCredentials).values({ hostAccessId, originalCredentialId, targetUserId, - encryptedUsername: "", // Will be filled during re-encryption + encryptedUsername: "", encryptedAuthType: "", needsReEncryption: true, }); @@ -697,7 +602,6 @@ class SharedCredentialManager { userId: string, ): Promise { try { - // Get the shared credential const sharedCred = await db .select() .from(sharedCredentials) @@ -714,7 +618,6 @@ class SharedCredentialManager { const cred = sharedCred[0]; - // Get the host access to find the owner const access = await db .select() .from(hostAccess) @@ -732,7 +635,6 @@ class SharedCredentialManager { const ownerId = access[0].ssh_data.userId; - // Get user's DEK (must be available) const userDEK = DataCrypto.getUserDataKey(userId); if (!userDEK) { databaseLogger.warn("Re-encrypt: user DEK not available", { @@ -740,29 +642,19 @@ class SharedCredentialManager { sharedCredId, userId, }); - // User offline, keep pending return; } - // Try owner's DEK first const ownerDEK = DataCrypto.getUserDataKey(ownerId); let credentialData: CredentialData; if (ownerDEK) { - // Owner online - use owner's DEK credentialData = await this.getDecryptedCredential( cred.originalCredentialId, ownerId, ownerDEK, ); } else { - // Owner offline - use system key fallback - databaseLogger.info("Re-encrypt: using system key (owner offline)", { - operation: "reencrypt_system_key", - sharedCredId, - ownerId, - }); - try { credentialData = await this.getDecryptedCredentialViaSystemKey( cred.originalCredentialId, @@ -776,12 +668,10 @@ class SharedCredentialManager { error: error instanceof Error ? error.message : "Unknown error", }, ); - // Keep pending if system fields don't exist yet return; } } - // Re-encrypt for user const encryptedForTarget = this.encryptCredentialForUser( credentialData, userId, @@ -789,7 +679,6 @@ class SharedCredentialManager { cred.hostAccessId, ); - // Update shared credential await db .update(sharedCredentials) .set({ @@ -798,12 +687,6 @@ class SharedCredentialManager { updatedAt: new Date().toISOString(), }) .where(eq(sharedCredentials.id, sharedCredId)); - - databaseLogger.info("Re-encrypted shared credential successfully", { - operation: "reencrypt_shared_credential", - sharedCredId, - userId, - }); } catch (error) { databaseLogger.error("Failed to re-encrypt shared credential", error, { operation: "reencrypt_shared_credential", diff --git a/src/backend/utils/simple-db-ops.ts b/src/backend/utils/simple-db-ops.ts index e8dfafb7..12fbee1b 100644 --- a/src/backend/utils/simple-db-ops.ts +++ b/src/backend/utils/simple-db-ops.ts @@ -28,7 +28,6 @@ class SimpleDBOps { userDataKey, ); - // Also encrypt with system key for ssh_credentials (offline sharing) if (tableName === "ssh_credentials") { const { SystemCrypto } = await import("./system-crypto.js"); const systemCrypto = SystemCrypto.getInstance(); @@ -125,7 +124,6 @@ class SimpleDBOps { userDataKey, ); - // Also encrypt with system key for ssh_credentials (offline sharing) if (tableName === "ssh_credentials") { const { SystemCrypto } = await import("./system-crypto.js"); const systemCrypto = SystemCrypto.getInstance(); diff --git a/src/backend/utils/socks5-helper.ts b/src/backend/utils/socks5-helper.ts index 8dd9d96c..c02f375e 100644 --- a/src/backend/utils/socks5-helper.ts +++ b/src/backend/utils/socks5-helper.ts @@ -25,22 +25,25 @@ export async function createSocks5Connection( targetPort: number, socks5Config: SOCKS5Config, ): Promise { - // If SOCKS5 is not enabled, return null if (!socks5Config.useSocks5) { return null; } - // If proxy chain is provided, use chain connection - if (socks5Config.socks5ProxyChain && socks5Config.socks5ProxyChain.length > 0) { - return createProxyChainConnection(targetHost, targetPort, socks5Config.socks5ProxyChain); + if ( + socks5Config.socks5ProxyChain && + socks5Config.socks5ProxyChain.length > 0 + ) { + return createProxyChainConnection( + targetHost, + targetPort, + socks5Config.socks5ProxyChain, + ); } - // If single proxy is configured, use single proxy connection if (socks5Config.socks5Host) { return createSingleProxyConnection(targetHost, targetPort, socks5Config); } - // No proxy configured return null; } @@ -67,24 +70,9 @@ async function createSingleProxyConnection( }, }; - sshLogger.info("Creating SOCKS5 connection", { - operation: "socks5_connect", - proxyHost: socks5Config.socks5Host, - proxyPort: socks5Config.socks5Port || 1080, - targetHost, - targetPort, - hasAuth: !!(socks5Config.socks5Username && socks5Config.socks5Password), - }); - try { const info = await SocksClient.createConnection(socksOptions); - sshLogger.info("SOCKS5 connection established", { - operation: "socks5_connected", - targetHost, - targetPort, - }); - return info.socket; } catch (error) { sshLogger.error("SOCKS5 connection failed", error, { @@ -113,14 +101,6 @@ async function createProxyChainConnection( } const chainPath = proxyChain.map((p) => `${p.host}:${p.port}`).join(" → "); - sshLogger.info(`Creating SOCKS proxy chain: ${chainPath} → ${targetHost}:${targetPort}`, { - operation: "socks5_chain_connect", - chainLength: proxyChain.length, - targetHost, - targetPort, - proxies: proxyChain.map((p) => `${p.host}:${p.port}`), - }); - try { const info = await SocksClient.createConnectionChain({ proxies: proxyChain.map((p) => ({ @@ -129,7 +109,7 @@ async function createProxyChainConnection( type: p.type, userId: p.username, password: p.password, - timeout: 10000, // 10-second timeout for each hop + timeout: 10000, })), command: "connect", destination: { @@ -137,15 +117,6 @@ async function createProxyChainConnection( port: targetPort, }, }); - - sshLogger.info(`✓ Proxy chain established: ${chainPath} → ${targetHost}:${targetPort}`, { - operation: "socks5_chain_connected", - chainLength: proxyChain.length, - targetHost, - targetPort, - fullPath: `${chainPath} → ${targetHost}:${targetPort}`, - }); - return info.socket; } catch (error) { sshLogger.error("SOCKS proxy chain connection failed", error, { diff --git a/src/constants/terminal-themes.ts b/src/constants/terminal-themes.ts index 6aec2e4a..46385c65 100644 --- a/src/constants/terminal-themes.ts +++ b/src/constants/terminal-themes.ts @@ -28,7 +28,6 @@ export interface TerminalTheme { } export const TERMINAL_THEMES: Record = { - // Legacy "termix" theme - auto-switches between termixDark and termixLight based on app theme termix: { name: "Termix Default", category: "dark", diff --git a/src/hooks/use-confirmation.ts b/src/hooks/use-confirmation.ts index 8d0918c1..46b9af66 100644 --- a/src/hooks/use-confirmation.ts +++ b/src/hooks/use-confirmation.ts @@ -39,13 +39,11 @@ export function useConfirmation() { opts: ConfirmationOptions | string, callback?: () => void, ): Promise => { - // Legacy signature support if (typeof opts === "string" && callback) { callback(); return Promise.resolve(true); } - // New Promise-based signature return Promise.resolve(true); }; diff --git a/src/index.css b/src/index.css index 7ce244ea..c5e0cf53 100644 --- a/src/index.css +++ b/src/index.css @@ -48,19 +48,18 @@ --sidebar-border: #e4e4e7; --sidebar-ring: #a1a1aa; - /* NEW SEMANTIC VARIABLES - Light Mode Backgrounds */ --bg-base: #fcfcfc; --bg-elevated: #ffffff; --bg-surface: #f3f4f6; - --bg-surface-hover: #e5e7eb; /* Panel hover - replaces dark-bg-panel-hover */ + --bg-surface-hover: #e5e7eb; --bg-input: #ffffff; --bg-deepest: #e5e7eb; --bg-header: #eeeeef; --bg-button: #f3f4f6; --bg-active: #e5e7eb; - --bg-light: #fafafa; /* Light background - replaces dark-bg-light */ - --bg-subtle: #f5f5f5; /* Very light background - replaces dark-bg-very-light */ - --bg-interact: #d1d5db; /* Interactive/active state - replaces dark-active */ + --bg-light: #fafafa; + --bg-subtle: #f5f5f5; + --bg-interact: #d1d5db; --border-base: #e5e7eb; --border-panel: #d1d5db; --border-subtle: #f3f4f6; @@ -71,16 +70,13 @@ --border-hover: #d1d5db; --border-active: #9ca3af; - /* NEW SEMANTIC VARIABLES - Light Mode Text Colors */ --foreground-secondary: #334155; --foreground-subtle: #94a3b8; - /* Scrollbar Colors - Light Mode */ --scrollbar-thumb: #c1c1c3; --scrollbar-thumb-hover: #a1a1a3; --scrollbar-track: #f3f4f6; - /* Modal Overlay - Light Mode */ --bg-overlay: rgba(0, 0, 0, 0.5); } @@ -143,8 +139,6 @@ --color-dark-border-panel: #222224; --color-dark-bg-panel-hover: #232327; - /* NEW SEMANTIC COLOR MAPPINGS - Creates Tailwind classes */ - /* Backgrounds: bg-canvas, bg-elevated, bg-surface, etc. */ --color-canvas: var(--bg-base); --color-elevated: var(--bg-elevated); --color-surface: var(--bg-surface); @@ -160,7 +154,7 @@ --color-hover: var(--bg-hover); --color-hover-alt: var(--bg-hover-alt); --color-pressed: var(--bg-pressed); - /* Borders: border-edge, border-edge-panel, etc. */ + --color-edge: var(--border-base); --color-edge-panel: var(--border-panel); --color-edge-subtle: var(--border-subtle); @@ -168,11 +162,9 @@ --color-edge-hover: var(--border-hover); --color-edge-active: var(--border-active); - /* NEW SEMANTIC TEXT COLOR MAPPINGS - Creates Tailwind text classes */ --color-foreground-secondary: var(--foreground-secondary); --color-foreground-subtle: var(--foreground-subtle); - /* Modal Overlay Mapping - Creates Tailwind bg-overlay class */ --color-overlay: var(--bg-overlay); } @@ -231,16 +223,13 @@ --border-hover: #434345; --border-active: #2d2d30; - /* NEW SEMANTIC VARIABLES - Dark Mode Text Color Overrides */ - --foreground-secondary: #d1d5db; /* Matches text-gray-300 */ - --foreground-subtle: #6b7280; /* Matches text-gray-500 */ + --foreground-secondary: #d1d5db; + --foreground-subtle: #6b7280; - /* Scrollbar Colors - Dark Mode */ --scrollbar-thumb: #434345; --scrollbar-thumb-hover: #5a5a5d; --scrollbar-track: #18181b; - /* Modal Overlay - Dark Mode */ --bg-overlay: rgba(0, 0, 0, 0.7); } @@ -259,7 +248,6 @@ } } -/* Thin Scrollbar - Theme Aware */ .thin-scrollbar { scrollbar-width: thin; scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); @@ -283,7 +271,6 @@ background: var(--scrollbar-thumb-hover); } -/* Skinny scrollbar - even thinner variant */ .skinny-scrollbar { scrollbar-width: thin; scrollbar-color: var(--scrollbar-thumb) transparent; diff --git a/src/lib/terminal-syntax-highlighter.ts b/src/lib/terminal-syntax-highlighter.ts index 26fb29fa..b9aef563 100644 --- a/src/lib/terminal-syntax-highlighter.ts +++ b/src/lib/terminal-syntax-highlighter.ts @@ -1,17 +1,3 @@ -/** - * Terminal Syntax Highlighter - * - * Adds syntax highlighting to terminal output by injecting ANSI color codes - * for common patterns like commands, paths, IPs, log levels, and keywords. - * - * Features: - * - Preserves existing ANSI codes from SSH output - * - Performance-optimized for streaming logs - * - Priority-based pattern matching to avoid overlaps - * - Configurable via localStorage - */ - -// ANSI escape code constants const ANSI_CODES = { reset: "\x1b[0m", colors: { @@ -22,7 +8,7 @@ const ANSI_CODES = { magenta: "\x1b[35m", cyan: "\x1b[36m", white: "\x1b[37m", - brightBlack: "\x1b[90m", // Gray + brightBlack: "\x1b[90m", brightRed: "\x1b[91m", brightGreen: "\x1b[92m", brightYellow: "\x1b[93m", @@ -39,16 +25,14 @@ const ANSI_CODES = { }, } as const; -// Pattern definition interface interface HighlightPattern { name: string; regex: RegExp; ansiCode: string; priority: number; - quickCheck?: string; // Optional fast string.includes() check + quickCheck?: string; } -// Match result interface for tracking ranges interface MatchResult { start: number; end: number; @@ -56,16 +40,10 @@ interface MatchResult { priority: number; } -// Configuration -const MAX_LINE_LENGTH = 5000; // Skip highlighting for very long lines -const MAX_ANSI_CODES = 10; // Skip if text has many ANSI codes (likely already colored/interactive app) +const MAX_LINE_LENGTH = 5000; +const MAX_ANSI_CODES = 10; -// Pattern definitions by category (pre-compiled) -// Based on SecureCRT proven patterns with strict boundaries const PATTERNS: HighlightPattern[] = [ - // Priority 1: IP Addresses (HIGHEST - from SecureCRT line 94) - // Matches: 192.168.1.1, 10.0.0.5, 127.0.0.1:8080 - // WON'T match: dates like "2025" or "03:11:36" { name: "ipv4", regex: @@ -74,7 +52,6 @@ const PATTERNS: HighlightPattern[] = [ priority: 10, }, - // Priority 2: Log Levels - Error (bright red) { name: "log-error", regex: @@ -83,7 +60,6 @@ const PATTERNS: HighlightPattern[] = [ priority: 9, }, - // Priority 3: Log Levels - Warning (yellow) { name: "log-warn", regex: /\b(WARN(?:ING)?|ALERT)\b|\[WARN(?:ING)?\]/gi, @@ -91,7 +67,6 @@ const PATTERNS: HighlightPattern[] = [ priority: 9, }, - // Priority 4: Log Levels - Success (bright green) { name: "log-success", regex: @@ -100,7 +75,6 @@ const PATTERNS: HighlightPattern[] = [ priority: 8, }, - // Priority 5: URLs (must start with http/https) { name: "url", regex: /https?:\/\/[^\s\])}]+/g, @@ -108,9 +82,6 @@ const PATTERNS: HighlightPattern[] = [ priority: 8, }, - // Priority 6: Absolute paths - STRICT (must have 2+ segments) - // Matches: /var/log/file.log, /home/user/docs - // WON'T match: /03, /2025, single segments { name: "path-absolute", regex: /\/[a-zA-Z][a-zA-Z0-9_\-@.]*(?:\/[a-zA-Z0-9_\-@.]+)+/g, @@ -118,7 +89,6 @@ const PATTERNS: HighlightPattern[] = [ priority: 7, }, - // Priority 7: Home paths { name: "path-home", regex: /~\/[a-zA-Z0-9_\-@./]+/g, @@ -126,7 +96,6 @@ const PATTERNS: HighlightPattern[] = [ priority: 7, }, - // Priority 8: Other log levels { name: "log-info", regex: /\bINFO\b|\[INFO\]/gi, @@ -141,11 +110,7 @@ const PATTERNS: HighlightPattern[] = [ }, ]; -/** - * Check if text contains existing ANSI escape sequences - */ function hasExistingAnsiCodes(text: string): boolean { - // Count all ANSI escape sequences (not just CSI) const ansiCount = ( text.match( /\x1b[\[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nq-uy=><~]/g, @@ -154,17 +119,10 @@ function hasExistingAnsiCodes(text: string): boolean { return ansiCount > MAX_ANSI_CODES; } -/** - * Check if text appears to be incomplete (partial ANSI sequence at end) - */ function hasIncompleteAnsiSequence(text: string): boolean { - // Check if text ends with incomplete ANSI escape sequence return /\x1b(?:\[(?:[0-9;]*)?)?$/.test(text); } -/** - * Parse text into segments: plain text and ANSI codes - */ interface TextSegment { isAnsi: boolean; content: string; @@ -172,13 +130,11 @@ interface TextSegment { function parseAnsiSegments(text: string): TextSegment[] { const segments: TextSegment[] = []; - // More comprehensive ANSI regex - matches SGR (colors), cursor movement, erase sequences, etc. const ansiRegex = /\x1b(?:[@-Z\\-_]|\[[0-9;]*[@-~])/g; let lastIndex = 0; let match; while ((match = ansiRegex.exec(text)) !== null) { - // Plain text before ANSI code if (match.index > lastIndex) { segments.push({ isAnsi: false, @@ -186,7 +142,6 @@ function parseAnsiSegments(text: string): TextSegment[] { }); } - // ANSI code itself segments.push({ isAnsi: true, content: match[0], @@ -195,7 +150,6 @@ function parseAnsiSegments(text: string): TextSegment[] { lastIndex = ansiRegex.lastIndex; } - // Remaining plain text if (lastIndex < text.length) { segments.push({ isAnsi: false, @@ -206,25 +160,18 @@ function parseAnsiSegments(text: string): TextSegment[] { return segments; } -/** - * Apply highlights to plain text (no ANSI codes) - */ function highlightPlainText(text: string): string { - // Skip very long lines for performance if (text.length > MAX_LINE_LENGTH) { return text; } - // Skip if text is empty or whitespace if (!text.trim()) { return text; } - // Find all matches for all patterns const matches: MatchResult[] = []; for (const pattern of PATTERNS) { - // Reset regex lastIndex pattern.regex.lastIndex = 0; let match; @@ -238,12 +185,10 @@ function highlightPlainText(text: string): string { } } - // If no matches, return original text if (matches.length === 0) { return text; } - // Sort matches by priority (descending) then by position matches.sort((a, b) => { if (a.priority !== b.priority) { return b.priority - a.priority; @@ -251,7 +196,6 @@ function highlightPlainText(text: string): string { return a.start - b.start; }); - // Filter out overlapping matches (keep higher priority) const appliedRanges: Array<{ start: number; end: number }> = []; const finalMatches = matches.filter((match) => { const overlaps = appliedRanges.some( @@ -268,7 +212,6 @@ function highlightPlainText(text: string): string { return false; }); - // Apply ANSI codes from end to start (to preserve indices) let result = text; finalMatches.reverse().forEach((match) => { const before = result.slice(0, match.start); @@ -281,41 +224,28 @@ function highlightPlainText(text: string): string { return result; } -/** - * Main export: Highlight terminal output text - * - * @param text - Terminal output text (may contain ANSI codes) - * @returns Text with syntax highlighting applied - */ export function highlightTerminalOutput(text: string): string { - // Early exit for empty or whitespace-only text if (!text || !text.trim()) { return text; } - // Skip highlighting if text has incomplete ANSI sequence (streaming chunk) if (hasIncompleteAnsiSequence(text)) { return text; } - // Skip highlighting if text already has many ANSI codes - // (likely already styled by SSH output or application) if (hasExistingAnsiCodes(text)) { return text; } - // Parse text into segments (plain text vs ANSI codes) const segments = parseAnsiSegments(text); - // If no ANSI codes found, highlight entire text if (segments.length === 0) { return highlightPlainText(text); } - // Highlight only plain text segments, preserve ANSI segments const highlightedSegments = segments.map((segment) => { if (segment.isAnsi) { - return segment.content; // Preserve existing ANSI codes + return segment.content; } else { return highlightPlainText(segment.content); } @@ -324,15 +254,10 @@ export function highlightTerminalOutput(text: string): string { return highlightedSegments.join(""); } -/** - * Check if syntax highlighting is enabled in localStorage - * Defaults to false if not set (opt-in behavior - BETA feature) - */ export function isSyntaxHighlightingEnabled(): boolean { try { return localStorage.getItem("terminalSyntaxHighlighting") === "true"; } catch { - // If localStorage is not available, default to disabled return false; } } diff --git a/src/locales/ar.json b/src/locales/ar.json index b1d77b08..ecd05635 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -2328,4 +2328,4 @@ "noContainersMatchFiltersHint": "التبديل إلى الوضع الداكن" }, "theme": {} -} \ No newline at end of file +} diff --git a/src/locales/bn.json b/src/locales/bn.json index cc3a2f19..f3a32b97 100644 --- a/src/locales/bn.json +++ b/src/locales/bn.json @@ -2379,4 +2379,4 @@ "switchToLight": "আলোতে স্যুইচ করুন", "switchToDark": "অন্ধকারে স্যুইচ করুন" } -} \ No newline at end of file +} diff --git a/src/locales/cs.json b/src/locales/cs.json index 64b20578..62d5b177 100644 --- a/src/locales/cs.json +++ b/src/locales/cs.json @@ -2379,4 +2379,4 @@ "switchToLight": "Spusťte kontejner pro přístup ke konzoli", "switchToDark": "Přepnout na světlou verzi" } -} \ No newline at end of file +} diff --git a/src/locales/de.json b/src/locales/de.json index 2124b2d4..90474129 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -2360,4 +2360,4 @@ "console": "Auf Dunkel umschalten" }, "theme": {} -} \ No newline at end of file +} diff --git a/src/locales/el.json b/src/locales/el.json index 14b1f99c..476bbe9b 100644 --- a/src/locales/el.json +++ b/src/locales/el.json @@ -2376,4 +2376,4 @@ "startContainerToAccess": "Μετάβαση σε σκούρο" }, "theme": {} -} \ No newline at end of file +} diff --git a/src/locales/es.json b/src/locales/es.json index 9a7fd940..036754a6 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -2379,4 +2379,4 @@ "switchToLight": "Cambiar a Claro", "switchToDark": "Cambiar a Oscuro" } -} \ No newline at end of file +} diff --git a/src/locales/fr.json b/src/locales/fr.json index dc03f7f5..ed479e31 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -2379,4 +2379,4 @@ "switchToLight": "Passer en mode clair", "switchToDark": "Passer en mode sombre" } -} \ No newline at end of file +} diff --git a/src/locales/he.json b/src/locales/he.json index 092e98df..85d6dba7 100644 --- a/src/locales/he.json +++ b/src/locales/he.json @@ -2379,4 +2379,4 @@ "switchToLight": "עבור לבהיר", "switchToDark": "עבור לכהה" } -} \ No newline at end of file +} diff --git a/src/locales/hi.json b/src/locales/hi.json index bd037a63..d5fb2585 100644 --- a/src/locales/hi.json +++ b/src/locales/hi.json @@ -2378,4 +2378,4 @@ "theme": { "switchToLight": "डार्क मोड पर स्विच करें" } -} \ No newline at end of file +} diff --git a/src/locales/id.json b/src/locales/id.json index dc9201e2..c5e3c028 100644 --- a/src/locales/id.json +++ b/src/locales/id.json @@ -2369,4 +2369,4 @@ "clickToConnect": "Beralih ke Gelap" }, "theme": {} -} \ No newline at end of file +} diff --git a/src/locales/it.json b/src/locales/it.json index f5a0667f..29fa3581 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -2379,4 +2379,4 @@ "switchToLight": "Passa a chiaro", "switchToDark": "Passa a scuro" } -} \ No newline at end of file +} diff --git a/src/locales/ja.json b/src/locales/ja.json index 964f82e8..f230207e 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -2379,4 +2379,4 @@ "switchToLight": "ライトモードに切り替える", "switchToDark": "ダークモードに切り替える" } -} \ No newline at end of file +} diff --git a/src/locales/ko.json b/src/locales/ko.json index 078e52e7..251402d9 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -2379,4 +2379,4 @@ "switchToLight": "라이트 모드로 전환", "switchToDark": "다크 모드로 전환" } -} \ No newline at end of file +} diff --git a/src/locales/nl.json b/src/locales/nl.json index d1da3c5c..4d0ee135 100644 --- a/src/locales/nl.json +++ b/src/locales/nl.json @@ -2371,4 +2371,4 @@ "containerNotFound": "Schakelen naar donker" }, "theme": {} -} \ No newline at end of file +} diff --git a/src/locales/pl.json b/src/locales/pl.json index 6aa211ce..d4a349ea 100644 --- a/src/locales/pl.json +++ b/src/locales/pl.json @@ -2358,4 +2358,4 @@ "errorMessage": "Przełącz na Ciemny" }, "theme": {} -} \ No newline at end of file +} diff --git a/src/locales/pt.json b/src/locales/pt.json index 75b5a023..fe4f7a97 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -2379,4 +2379,4 @@ "switchToLight": "Alternar para o modo claro", "switchToDark": "Alternar para o modo escuro" } -} \ No newline at end of file +} diff --git a/src/locales/ro.json b/src/locales/ro.json index bfd18ceb..298b2f20 100644 --- a/src/locales/ro.json +++ b/src/locales/ro.json @@ -2375,4 +2375,4 @@ "consoleTab": "Comutați pe Întunecat" }, "theme": {} -} \ No newline at end of file +} diff --git a/src/locales/ru.json b/src/locales/ru.json index 8984e7d9..81ae39e7 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -2379,4 +2379,4 @@ "switchToLight": "Переключиться на светлый режим", "switchToDark": "Переключиться на темный режим" } -} \ No newline at end of file +} diff --git a/src/locales/sv.json b/src/locales/sv.json index 26a7c417..c8c60b5a 100644 --- a/src/locales/sv.json +++ b/src/locales/sv.json @@ -2371,4 +2371,4 @@ "containerNotFound": "Växla till mörk" }, "theme": {} -} \ No newline at end of file +} diff --git a/src/locales/th.json b/src/locales/th.json index 784dbcb9..b0c7fe46 100644 --- a/src/locales/th.json +++ b/src/locales/th.json @@ -2371,4 +2371,4 @@ "containerNotFound": "สลับเป็นโหมดมืด" }, "theme": {} -} \ No newline at end of file +} diff --git a/src/locales/tr.json b/src/locales/tr.json index 48674016..5b9e60c2 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -2347,4 +2347,4 @@ "pids": "Koyu moda geç" }, "theme": {} -} \ No newline at end of file +} diff --git a/src/locales/uk.json b/src/locales/uk.json index f2aab5cf..28c882fb 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -2379,4 +2379,4 @@ "switchToLight": "Переключитися на світлий режим", "switchToDark": "Переключитися на темний режим" } -} \ No newline at end of file +} diff --git a/src/locales/vi.json b/src/locales/vi.json index 1571bdae..d764e544 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -2378,4 +2378,4 @@ "theme": { "switchToLight": "Chuyển sang Tối" } -} \ No newline at end of file +} diff --git a/src/locales/zh.json b/src/locales/zh.json index 8ed6414b..94450148 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -2379,4 +2379,4 @@ "switchToLight": "切换到浅色模式", "switchToDark": "切换到深色模式" } -} \ No newline at end of file +} diff --git a/src/types/index.ts b/src/types/index.ts index 9c23e807..945f1927 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -60,7 +60,6 @@ export interface SSHHost { createdAt: string; updatedAt: string; - // Shared access metadata (view-only) isShared?: boolean; permissionLevel?: "view"; sharedExpiresAt?: string; @@ -78,7 +77,7 @@ export interface QuickActionData { export interface ProxyNode { host: string; port: number; - type: 4 | 5; // SOCKS4 or SOCKS5 + type: 4 | 5; username?: string; password?: string; } @@ -112,7 +111,6 @@ export interface SSHHostData { terminalConfig?: TerminalConfig; notes?: string; - // SOCKS5 Proxy configuration useSocks5?: boolean; socks5Host?: string; socks5Port?: number; @@ -213,11 +211,9 @@ export interface TunnelConnection { export interface TunnelConfig { name: string; - // Unique identifiers for collision prevention sourceHostId: number; tunnelIndex: number; - // User context for RBAC requestingUserId?: string; hostName: string; @@ -249,7 +245,6 @@ export interface TunnelConfig { autoStart: boolean; isPinned: boolean; - // SOCKS5 Proxy configuration useSocks5?: boolean; socks5Host?: string; socks5Port?: number; @@ -418,7 +413,7 @@ export interface SplitLayoutOption { name: string; description: string; cellCount: number; - icon: string; // lucide icon name + icon: string; } // ============================================================================ diff --git a/src/ui/desktop/DesktopApp.tsx b/src/ui/desktop/DesktopApp.tsx index 06669556..13ddf982 100644 --- a/src/ui/desktop/DesktopApp.tsx +++ b/src/ui/desktop/DesktopApp.tsx @@ -43,8 +43,6 @@ function AppContent() { const lastShiftPressTime = useRef(0); - // DEBUG: Theme toggle - double-tap left Alt/Option to toggle light/dark mode - // Comment out the next line and the AltLeft handler below to disable const lastAltPressTime = useRef(0); useEffect(() => { @@ -62,26 +60,20 @@ function AppContent() { } } - // DEBUG: Double-tap left Alt/Option to toggle light/dark theme - // Remove or comment out this block for production - /* DEBUG_THEME_TOGGLE_START */ if (event.code === "AltLeft" && !event.repeat) { const now = Date.now(); if (now - lastAltPressTime.current < 300) { - // Use setTheme to properly update React state (not just DOM class) const currentIsDark = theme === "dark" || (theme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches); const newTheme = currentIsDark ? "light" : "dark"; setTheme(newTheme); - console.log("[DEBUG] Theme toggled:", newTheme); lastAltPressTime.current = 0; } else { lastAltPressTime.current = now; } } - /* DEBUG_THEME_TOGGLE_END */ if (event.key === "Escape") { setIsCommandPaletteOpen(false); diff --git a/src/ui/desktop/apps/admin/AdminSettings.tsx b/src/ui/desktop/apps/admin/AdminSettings.tsx index c8c5f31c..8f226339 100644 --- a/src/ui/desktop/apps/admin/AdminSettings.tsx +++ b/src/ui/desktop/apps/admin/AdminSettings.tsx @@ -72,7 +72,6 @@ export function AdminSettings({ >([]); const [usersLoading, setUsersLoading] = React.useState(false); - // New dialog states const [createUserDialogOpen, setCreateUserDialogOpen] = React.useState(false); const [userEditDialogOpen, setUserEditDialogOpen] = React.useState(false); const [selectedUserForEdit, setSelectedUserForEdit] = React.useState<{ @@ -216,7 +215,6 @@ export function AdminSettings({ } }; - // New dialog handlers const handleEditUser = (user: (typeof users)[0]) => { setSelectedUserForEdit(user); setUserEditDialogOpen(true); diff --git a/src/ui/desktop/apps/admin/dialogs/CreateUserDialog.tsx b/src/ui/desktop/apps/admin/dialogs/CreateUserDialog.tsx index 6f3e9cb7..47e5cc22 100644 --- a/src/ui/desktop/apps/admin/dialogs/CreateUserDialog.tsx +++ b/src/ui/desktop/apps/admin/dialogs/CreateUserDialog.tsx @@ -34,7 +34,6 @@ export function CreateUserDialog({ const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - // Reset form when dialog closes useEffect(() => { if (!open) { setUsername(""); diff --git a/src/ui/desktop/apps/admin/dialogs/LinkAccountDialog.tsx b/src/ui/desktop/apps/admin/dialogs/LinkAccountDialog.tsx index 6e285748..b99b0950 100644 --- a/src/ui/desktop/apps/admin/dialogs/LinkAccountDialog.tsx +++ b/src/ui/desktop/apps/admin/dialogs/LinkAccountDialog.tsx @@ -33,7 +33,6 @@ export function LinkAccountDialog({ const [linkTargetUsername, setLinkTargetUsername] = useState(""); const [linkLoading, setLinkLoading] = useState(false); - // Reset form when dialog closes useEffect(() => { if (!open) { setLinkTargetUsername(""); diff --git a/src/ui/desktop/apps/admin/dialogs/UserEditDialog.tsx b/src/ui/desktop/apps/admin/dialogs/UserEditDialog.tsx index c01d4edf..3b525f1f 100644 --- a/src/ui/desktop/apps/admin/dialogs/UserEditDialog.tsx +++ b/src/ui/desktop/apps/admin/dialogs/UserEditDialog.tsx @@ -114,7 +114,6 @@ export function UserEditDialog({ return; } - // Close dialog temporarily to show confirmation toast on top const userToUpdate = user; onOpenChange(false); @@ -165,7 +164,6 @@ export function UserEditDialog({ const handlePasswordReset = async () => { if (!user) return; - // Close dialog temporarily to show confirmation toast on top const userToReset = user; onOpenChange(false); @@ -217,7 +215,6 @@ export function UserEditDialog({ const handleRemoveRole = async (roleId: number) => { if (!user) return; - // Close dialog temporarily to show confirmation toast on top const userToUpdate = user; onOpenChange(false); @@ -253,7 +250,6 @@ export function UserEditDialog({ const isRevokingSelf = isCurrentUser; - // Close dialog temporarily to show confirmation toast on top const userToUpdate = user; onOpenChange(false); @@ -302,7 +298,6 @@ export function UserEditDialog({ return; } - // Close dialog temporarily to show confirmation toast on top const userToDelete = user; onOpenChange(false); @@ -315,7 +310,6 @@ export function UserEditDialog({ }); if (!confirmed) { - // Reopen dialog if user cancels onOpenChange(true); return; } @@ -366,7 +360,6 @@ export function UserEditDialog({
- {/* READ-ONLY INFO SECTION */}