fix: replace explicit any types with proper TypeScript types

- Replace 'any' with 'unknown' in catch blocks and add type assertions
- Create explicit interfaces for complex objects (HostConfig, TabData, TerminalHandle)
- Fix window/document object type extensions
- Update Electron API type definitions
- Improve type safety in database routes and utilities
- Add proper types to Terminal components (Desktop & Mobile)
- Fix navigation component types (TopNavbar, LeftSidebar, AppView)

Reduces TypeScript lint errors from 394 to 358 (-36 errors)
Fixes 45 @typescript-eslint/no-explicit-any violations
This commit is contained in:
ZacharyZcR
2025-10-09 18:06:17 +08:00
parent 1decac481e
commit d7e98cda04
22 changed files with 2002 additions and 1540 deletions

View File

@@ -31,8 +31,13 @@ import {
dismissedAlerts, dismissedAlerts,
sshCredentialUsage, sshCredentialUsage,
settings, settings,
snippets,
} from "./db/schema.js"; } from "./db/schema.js";
import type {
CacheEntry,
GitHubRelease,
GitHubAPIResponse,
AuthenticatedRequest,
} from "../../types/index.js";
import { getDb } from "./db/index.js"; import { getDb } from "./db/index.js";
import Database from "better-sqlite3"; import Database from "better-sqlite3";
@@ -107,17 +112,11 @@ const upload = multer({
}, },
}); });
interface CacheEntry {
data: any;
timestamp: number;
expiresAt: number;
}
class GitHubCache { class GitHubCache {
private cache: Map<string, CacheEntry> = new Map(); private cache: Map<string, CacheEntry> = new Map();
private readonly CACHE_DURATION = 30 * 60 * 1000; private readonly CACHE_DURATION = 30 * 60 * 1000;
set(key: string, data: any): void { set<T>(key: string, data: T): void {
const now = Date.now(); const now = Date.now();
this.cache.set(key, { this.cache.set(key, {
data, data,
@@ -126,7 +125,7 @@ class GitHubCache {
}); });
} }
get(key: string): any | null { get<T>(key: string): T | null {
const entry = this.cache.get(key); const entry = this.cache.get(key);
if (!entry) { if (!entry) {
return null; return null;
@@ -137,7 +136,7 @@ class GitHubCache {
return null; return null;
} }
return entry.data; return entry.data as T;
} }
} }
@@ -147,34 +146,16 @@ const GITHUB_API_BASE = "https://api.github.com";
const REPO_OWNER = "LukeGus"; const REPO_OWNER = "LukeGus";
const REPO_NAME = "Termix"; const REPO_NAME = "Termix";
interface GitHubRelease { async function fetchGitHubAPI<T>(
id: number;
tag_name: string;
name: string;
body: string;
published_at: string;
html_url: string;
assets: Array<{
id: number;
name: string;
size: number;
download_count: number;
browser_download_url: string;
}>;
prerelease: boolean;
draft: boolean;
}
async function fetchGitHubAPI(
endpoint: string, endpoint: string,
cacheKey: string, cacheKey: string,
): Promise<any> { ): Promise<GitHubAPIResponse<T>> {
const cachedData = githubCache.get(cacheKey); const cachedEntry = githubCache.get<CacheEntry<T>>(cacheKey);
if (cachedData) { if (cachedEntry) {
return { return {
data: cachedData, data: cachedEntry.data,
cached: true, cached: true,
cache_age: Date.now() - cachedData.timestamp, cache_age: Date.now() - cachedEntry.timestamp,
}; };
} }
@@ -193,8 +174,13 @@ async function fetchGitHubAPI(
); );
} }
const data = await response.json(); const data = (await response.json()) as T;
githubCache.set(cacheKey, data); const cacheData: CacheEntry<T> = {
data,
timestamp: Date.now(),
expiresAt: Date.now() + 30 * 60 * 1000,
};
githubCache.set(cacheKey, cacheData);
return { return {
data: data, data: data,
@@ -274,7 +260,7 @@ app.get("/version", authenticateJWT, async (req, res) => {
try { try {
const cacheKey = "latest_release"; const cacheKey = "latest_release";
const releaseData = await fetchGitHubAPI( const releaseData = await fetchGitHubAPI<GitHubRelease>(
`/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`, `/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`,
cacheKey, cacheKey,
); );
@@ -325,12 +311,12 @@ app.get("/releases/rss", authenticateJWT, async (req, res) => {
); );
const cacheKey = `releases_rss_${page}_${per_page}`; const cacheKey = `releases_rss_${page}_${per_page}`;
const releasesData = await fetchGitHubAPI( const releasesData = await fetchGitHubAPI<GitHubRelease[]>(
`/repos/${REPO_OWNER}/${REPO_NAME}/releases?page=${page}&per_page=${per_page}`, `/repos/${REPO_OWNER}/${REPO_NAME}/releases?page=${page}&per_page=${per_page}`,
cacheKey, cacheKey,
); );
const rssItems = releasesData.data.map((release: GitHubRelease) => ({ const rssItems = releasesData.data.map((release) => ({
id: release.id, id: release.id,
title: release.name || release.tag_name, title: release.name || release.tag_name,
description: release.body, description: release.body,
@@ -459,7 +445,7 @@ app.post("/encryption/regenerate-jwt", requireAdmin, async (req, res) => {
app.post("/database/export", authenticateJWT, async (req, res) => { app.post("/database/export", authenticateJWT, async (req, res) => {
try { try {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { password } = req.body; const { password } = req.body;
if (!password) { if (!password) {
@@ -913,7 +899,7 @@ app.post(
return res.status(400).json({ error: "No file uploaded" }); return res.status(400).json({ error: "No file uploaded" });
} }
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { password } = req.body; const { password } = req.body;
if (!password) { if (!password) {
@@ -1321,7 +1307,7 @@ app.post(
apiLogger.error("SQLite import failed", error, { apiLogger.error("SQLite import failed", error, {
operation: "sqlite_import_api_failed", operation: "sqlite_import_api_failed",
userId: (req as any).userId, userId: (req as AuthenticatedRequest).userId,
}); });
res.status(500).json({ res.status(500).json({
error: "Failed to import SQLite data", error: "Failed to import SQLite data",
@@ -1333,7 +1319,7 @@ app.post(
app.post("/database/export/preview", authenticateJWT, async (req, res) => { app.post("/database/export/preview", authenticateJWT, async (req, res) => {
try { try {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { scope = "user_data", includeCredentials = true } = req.body; const { scope = "user_data", includeCredentials = true } = req.body;
const exportData = await UserDataExport.exportUserData(userId, { const exportData = await UserDataExport.exportUserData(userId, {

View File

@@ -1,3 +1,8 @@
import type {
AuthenticatedRequest,
CacheEntry,
TermixAlert,
} from "../../../types/index.js";
import express from "express"; import express from "express";
import { db } from "../db/index.js"; import { db } from "../db/index.js";
import { dismissedAlerts } from "../db/schema.js"; import { dismissedAlerts } from "../db/schema.js";
@@ -6,17 +11,11 @@ import fetch from "node-fetch";
import { authLogger } from "../../utils/logger.js"; import { authLogger } from "../../utils/logger.js";
import { AuthManager } from "../../utils/auth-manager.js"; import { AuthManager } from "../../utils/auth-manager.js";
interface CacheEntry {
data: any;
timestamp: number;
expiresAt: number;
}
class AlertCache { class AlertCache {
private cache: Map<string, CacheEntry> = new Map(); private cache: Map<string, CacheEntry> = new Map();
private readonly CACHE_DURATION = 5 * 60 * 1000; private readonly CACHE_DURATION = 5 * 60 * 1000;
set(key: string, data: any): void { set<T>(key: string, data: T): void {
const now = Date.now(); const now = Date.now();
this.cache.set(key, { this.cache.set(key, {
data, data,
@@ -25,7 +24,7 @@ class AlertCache {
}); });
} }
get(key: string): any | null { get<T>(key: string): T | null {
const entry = this.cache.get(key); const entry = this.cache.get(key);
if (!entry) { if (!entry) {
return null; return null;
@@ -36,7 +35,7 @@ class AlertCache {
return null; return null;
} }
return entry.data; return entry.data as T;
} }
} }
@@ -47,20 +46,9 @@ const REPO_OWNER = "LukeGus";
const REPO_NAME = "Termix-Docs"; const REPO_NAME = "Termix-Docs";
const ALERTS_FILE = "main/termix-alerts.json"; const ALERTS_FILE = "main/termix-alerts.json";
interface TermixAlert {
id: string;
title: string;
message: string;
expiresAt: string;
priority?: "low" | "medium" | "high" | "critical";
type?: "info" | "warning" | "error" | "success";
actionUrl?: string;
actionText?: string;
}
async function fetchAlertsFromGitHub(): Promise<TermixAlert[]> { async function fetchAlertsFromGitHub(): Promise<TermixAlert[]> {
const cacheKey = "termix_alerts"; const cacheKey = "termix_alerts";
const cachedData = alertCache.get(cacheKey); const cachedData = alertCache.get<TermixAlert[]>(cacheKey);
if (cachedData) { if (cachedData) {
return cachedData; return cachedData;
} }
@@ -115,7 +103,7 @@ const authenticateJWT = authManager.createAuthMiddleware();
// GET /alerts // GET /alerts
router.get("/", authenticateJWT, async (req, res) => { router.get("/", authenticateJWT, async (req, res) => {
try { try {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const allAlerts = await fetchAlertsFromGitHub(); const allAlerts = await fetchAlertsFromGitHub();
@@ -148,7 +136,7 @@ router.get("/", authenticateJWT, async (req, res) => {
router.post("/dismiss", authenticateJWT, async (req, res) => { router.post("/dismiss", authenticateJWT, async (req, res) => {
try { try {
const { alertId } = req.body; const { alertId } = req.body;
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!alertId) { if (!alertId) {
authLogger.warn("Missing alertId in dismiss request", { userId }); authLogger.warn("Missing alertId in dismiss request", { userId });
@@ -186,7 +174,7 @@ router.post("/dismiss", authenticateJWT, async (req, res) => {
// GET /alerts/dismissed/:userId // GET /alerts/dismissed/:userId
router.get("/dismissed", authenticateJWT, async (req, res) => { router.get("/dismissed", authenticateJWT, async (req, res) => {
try { try {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const dismissedAlertRecords = await db const dismissedAlertRecords = await db
.select({ .select({
@@ -211,7 +199,7 @@ router.get("/dismissed", authenticateJWT, async (req, res) => {
router.delete("/dismiss", authenticateJWT, async (req, res) => { router.delete("/dismiss", authenticateJWT, async (req, res) => {
try { try {
const { alertId } = req.body; const { alertId } = req.body;
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!alertId) { if (!alertId) {
return res.status(400).json({ error: "Alert ID is required" }); return res.status(400).json({ error: "Alert ID is required" });

View File

@@ -1,3 +1,4 @@
import type { AuthenticatedRequest } from "../../../types/index.js";
import express from "express"; import express from "express";
import { db } from "../db/index.js"; import { db } from "../db/index.js";
import { sshCredentials, sshCredentialUsage, sshData } from "../db/schema.js"; import { sshCredentials, sshCredentialUsage, sshData } from "../db/schema.js";
@@ -27,7 +28,11 @@ function generateSSHKeyPair(
} { } {
try { try {
let ssh2Type = keyType; let ssh2Type = keyType;
const options: any = {}; const options: {
bits?: number;
passphrase?: string;
cipher?: string;
} = {};
if (keyType === "ssh-rsa") { if (keyType === "ssh-rsa") {
ssh2Type = "rsa"; ssh2Type = "rsa";
@@ -44,6 +49,7 @@ function generateSSHKeyPair(
options.cipher = "aes128-cbc"; options.cipher = "aes128-cbc";
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const keyPair = ssh2Utils.generateKeyPairSync(ssh2Type as any, options); const keyPair = ssh2Utils.generateKeyPairSync(ssh2Type as any, options);
return { return {
@@ -62,7 +68,7 @@ function generateSSHKeyPair(
const router = express.Router(); const router = express.Router();
function isNonEmptyString(val: any): val is string { function isNonEmptyString(val: unknown): val is string {
return typeof val === "string" && val.trim().length > 0; return typeof val === "string" && val.trim().length > 0;
} }
@@ -77,7 +83,7 @@ router.post(
authenticateJWT, authenticateJWT,
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { const {
name, name,
description, description,
@@ -224,7 +230,7 @@ router.get(
authenticateJWT, authenticateJWT,
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!isNonEmptyString(userId)) { if (!isNonEmptyString(userId)) {
authLogger.warn("Invalid userId for credential fetch"); authLogger.warn("Invalid userId for credential fetch");
@@ -257,7 +263,7 @@ router.get(
authenticateJWT, authenticateJWT,
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!isNonEmptyString(userId)) { if (!isNonEmptyString(userId)) {
authLogger.warn("Invalid userId for credential folder fetch"); authLogger.warn("Invalid userId for credential folder fetch");
@@ -295,7 +301,7 @@ router.get(
authenticateJWT, authenticateJWT,
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { id } = req.params; const { id } = req.params;
if (!isNonEmptyString(userId) || !id) { if (!isNonEmptyString(userId) || !id) {
@@ -326,19 +332,19 @@ router.get(
const output = formatCredentialOutput(credential); const output = formatCredentialOutput(credential);
if (credential.password) { if (credential.password) {
(output as any).password = credential.password; output.password = credential.password;
} }
if (credential.key) { if (credential.key) {
(output as any).key = credential.key; output.key = credential.key;
} }
if (credential.private_key) { if (credential.private_key) {
(output as any).privateKey = credential.private_key; output.privateKey = credential.private_key;
} }
if (credential.public_key) { if (credential.public_key) {
(output as any).publicKey = credential.public_key; output.publicKey = credential.public_key;
} }
if (credential.key_password) { if (credential.key_password) {
(output as any).keyPassword = credential.key_password; output.keyPassword = credential.key_password;
} }
res.json(output); res.json(output);
@@ -359,7 +365,7 @@ router.put(
authenticateJWT, authenticateJWT,
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { id } = req.params; const { id } = req.params;
const updateData = req.body; const updateData = req.body;
@@ -383,7 +389,7 @@ router.put(
return res.status(404).json({ error: "Credential not found" }); return res.status(404).json({ error: "Credential not found" });
} }
const updateFields: any = {}; const updateFields: Record<string, string | null | undefined> = {};
if (updateData.name !== undefined) if (updateData.name !== undefined)
updateFields.name = updateData.name.trim(); updateFields.name = updateData.name.trim();
@@ -495,7 +501,7 @@ router.delete(
authenticateJWT, authenticateJWT,
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { id } = req.params; const { id } = req.params;
if (!isNonEmptyString(userId) || !id) { if (!isNonEmptyString(userId) || !id) {
@@ -594,7 +600,7 @@ router.post(
"/:id/apply-to-host/:hostId", "/:id/apply-to-host/:hostId",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { id: credentialId, hostId } = req.params; const { id: credentialId, hostId } = req.params;
if (!isNonEmptyString(userId) || !credentialId || !hostId) { if (!isNonEmptyString(userId) || !credentialId || !hostId) {
@@ -627,8 +633,8 @@ router.post(
.update(sshData) .update(sshData)
.set({ .set({
credentialId: parseInt(credentialId), credentialId: parseInt(credentialId),
username: credential.username, username: credential.username as string,
authType: credential.auth_type || credential.authType, authType: (credential.auth_type || credential.authType) as string,
password: null, password: null,
key: null, key: null,
key_password: null, key_password: null,
@@ -673,7 +679,7 @@ router.get(
"/:id/hosts", "/:id/hosts",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { id: credentialId } = req.params; const { id: credentialId } = req.params;
if (!isNonEmptyString(userId) || !credentialId) { if (!isNonEmptyString(userId) || !credentialId) {
@@ -705,7 +711,9 @@ router.get(
}, },
); );
function formatCredentialOutput(credential: any): any { function formatCredentialOutput(
credential: Record<string, unknown>,
): Record<string, unknown> {
return { return {
id: credential.id, id: credential.id,
name: credential.name, name: credential.name,
@@ -729,7 +737,9 @@ function formatCredentialOutput(credential: any): any {
}; };
} }
function formatSSHHostOutput(host: any): any { function formatSSHHostOutput(
host: Record<string, unknown>,
): Record<string, unknown> {
return { return {
id: host.id, id: host.id,
userId: host.userId, userId: host.userId,
@@ -749,7 +759,7 @@ function formatSSHHostOutput(host: any): any {
enableTerminal: !!host.enableTerminal, enableTerminal: !!host.enableTerminal,
enableTunnel: !!host.enableTunnel, enableTunnel: !!host.enableTunnel,
tunnelConnections: host.tunnelConnections tunnelConnections: host.tunnelConnections
? JSON.parse(host.tunnelConnections) ? JSON.parse(host.tunnelConnections as string)
: [], : [],
enableFileManager: !!host.enableFileManager, enableFileManager: !!host.enableFileManager,
defaultPath: host.defaultPath, defaultPath: host.defaultPath,
@@ -764,7 +774,7 @@ router.put(
"/folders/rename", "/folders/rename",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { oldName, newName } = req.body; const { oldName, newName } = req.body;
if (!isNonEmptyString(oldName) || !isNonEmptyString(newName)) { if (!isNonEmptyString(oldName) || !isNonEmptyString(newName)) {
@@ -1117,10 +1127,10 @@ router.post(
); );
async function deploySSHKeyToHost( async function deploySSHKeyToHost(
hostConfig: any, hostConfig: Record<string, unknown>,
publicKey: string, publicKey: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
_credentialData: any, _credentialData: Record<string, unknown>,
): Promise<{ success: boolean; message?: string; error?: string }> { ): Promise<{ success: boolean; message?: string; error?: string }> {
return new Promise((resolve) => { return new Promise((resolve) => {
const conn = new Client(); const conn = new Client();
@@ -1364,7 +1374,7 @@ async function deploySSHKeyToHost(
}); });
try { try {
const connectionConfig: any = { const connectionConfig: Record<string, unknown> = {
host: hostConfig.ip, host: hostConfig.ip,
port: hostConfig.port || 22, port: hostConfig.port || 22,
username: hostConfig.username, username: hostConfig.username,
@@ -1411,14 +1421,15 @@ async function deploySSHKeyToHost(
connectionConfig.password = hostConfig.password; connectionConfig.password = hostConfig.password;
} else if (hostConfig.authType === "key" && hostConfig.privateKey) { } else if (hostConfig.authType === "key" && hostConfig.privateKey) {
try { try {
const privateKey = hostConfig.privateKey as string;
if ( if (
!hostConfig.privateKey.includes("-----BEGIN") || !privateKey.includes("-----BEGIN") ||
!hostConfig.privateKey.includes("-----END") !privateKey.includes("-----END")
) { ) {
throw new Error("Invalid private key format"); throw new Error("Invalid private key format");
} }
const cleanKey = hostConfig.privateKey const cleanKey = privateKey
.trim() .trim()
.replace(/\r\n/g, "\n") .replace(/\r\n/g, "\n")
.replace(/\r/g, "\n"); .replace(/\r/g, "\n");
@@ -1473,7 +1484,7 @@ router.post(
} }
try { try {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!userId) { if (!userId) {
return res.status(401).json({ return res.status(401).json({
success: false, success: false,
@@ -1540,7 +1551,7 @@ router.post(
}; };
if (hostData.authType === "credential" && hostData.credentialId) { if (hostData.authType === "credential" && hostData.credentialId) {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!userId) { if (!userId) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
@@ -1554,7 +1565,7 @@ router.post(
db db
.select() .select()
.from(sshCredentials) .from(sshCredentials)
.where(eq(sshCredentials.id, hostData.credentialId)) .where(eq(sshCredentials.id, hostData.credentialId as number))
.limit(1), .limit(1),
"ssh_credentials", "ssh_credentials",
userId, userId,
@@ -1589,7 +1600,7 @@ router.post(
const deployResult = await deploySSHKeyToHost( const deployResult = await deploySSHKeyToHost(
hostConfig, hostConfig,
credData.publicKey, credData.publicKey as string,
credData, credData,
); );

View File

@@ -1,3 +1,4 @@
import type { AuthenticatedRequest } from "../../../types/index.js";
import express from "express"; import express from "express";
import { db } from "../db/index.js"; import { db } from "../db/index.js";
import { snippets } from "../db/schema.js"; import { snippets } from "../db/schema.js";
@@ -8,7 +9,7 @@ import { AuthManager } from "../../utils/auth-manager.js";
const router = express.Router(); const router = express.Router();
function isNonEmptyString(val: any): val is string { function isNonEmptyString(val: unknown): val is string {
return typeof val === "string" && val.trim().length > 0; return typeof val === "string" && val.trim().length > 0;
} }
@@ -23,7 +24,7 @@ router.get(
authenticateJWT, authenticateJWT,
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!isNonEmptyString(userId)) { if (!isNonEmptyString(userId)) {
authLogger.warn("Invalid userId for snippets fetch"); authLogger.warn("Invalid userId for snippets fetch");
@@ -52,12 +53,15 @@ router.get(
authenticateJWT, authenticateJWT,
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { id } = req.params; const { id } = req.params;
const snippetId = parseInt(id, 10); const snippetId = parseInt(id, 10);
if (!isNonEmptyString(userId) || isNaN(snippetId)) { if (!isNonEmptyString(userId) || isNaN(snippetId)) {
authLogger.warn("Invalid request for snippet fetch: invalid ID", { userId, id }); authLogger.warn("Invalid request for snippet fetch: invalid ID", {
userId,
id,
});
return res.status(400).json({ error: "Invalid request parameters" }); return res.status(400).json({ error: "Invalid request parameters" });
} }
@@ -88,7 +92,7 @@ router.post(
authenticateJWT, authenticateJWT,
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { name, content, description } = req.body; const { name, content, description } = req.body;
if ( if (
@@ -139,7 +143,7 @@ router.put(
authenticateJWT, authenticateJWT,
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { id } = req.params; const { id } = req.params;
const updateData = req.body; const updateData = req.body;
@@ -158,7 +162,12 @@ router.put(
return res.status(404).json({ error: "Snippet not found" }); return res.status(404).json({ error: "Snippet not found" });
} }
const updateFields: any = { const updateFields: Partial<{
updatedAt: ReturnType<typeof sql.raw>;
name: string;
content: string;
description: string | null;
}> = {
updatedAt: sql`CURRENT_TIMESTAMP`, updatedAt: sql`CURRENT_TIMESTAMP`,
}; };
@@ -206,7 +215,7 @@ router.delete(
authenticateJWT, authenticateJWT,
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { id } = req.params; const { id } = req.params;
if (!isNonEmptyString(userId) || !id) { if (!isNonEmptyString(userId) || !id) {

View File

@@ -1,3 +1,4 @@
import type { AuthenticatedRequest } from "../../../types/index.js";
import express from "express"; import express from "express";
import { db } from "../db/index.js"; import { db } from "../db/index.js";
import { import {
@@ -22,11 +23,11 @@ const router = express.Router();
const upload = multer({ storage: multer.memoryStorage() }); const upload = multer({ storage: multer.memoryStorage() });
function isNonEmptyString(value: any): value is string { function isNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0; return typeof value === "string" && value.trim().length > 0;
} }
function isValidPort(port: any): port is number { function isValidPort(port: unknown): port is number {
return typeof port === "number" && port > 0 && port <= 65535; return typeof port === "number" && port > 0 && port <= 65535;
} }
@@ -74,7 +75,7 @@ router.get("/db/host/internal", async (req: Request, res: Response) => {
: []; : [];
const hasAutoStartTunnels = tunnelConnections.some( const hasAutoStartTunnels = tunnelConnections.some(
(tunnel: any) => tunnel.autoStart, (tunnel: Record<string, unknown>) => tunnel.autoStart,
); );
if (!hasAutoStartTunnels) { if (!hasAutoStartTunnels) {
@@ -99,7 +100,7 @@ router.get("/db/host/internal", async (req: Request, res: Response) => {
credentialId: host.credentialId, credentialId: host.credentialId,
enableTunnel: true, enableTunnel: true,
tunnelConnections: tunnelConnections.filter( tunnelConnections: tunnelConnections.filter(
(tunnel: any) => tunnel.autoStart, (tunnel: Record<string, unknown>) => tunnel.autoStart,
), ),
pin: !!host.pin, pin: !!host.pin,
enableTerminal: !!host.enableTerminal, enableTerminal: !!host.enableTerminal,
@@ -183,8 +184,8 @@ router.post(
requireDataAccess, requireDataAccess,
upload.single("key"), upload.single("key"),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
let hostData: any; let hostData: Record<string, unknown>;
if (req.headers["content-type"]?.includes("multipart/form-data")) { if (req.headers["content-type"]?.includes("multipart/form-data")) {
if (req.body.data) { if (req.body.data) {
@@ -251,7 +252,7 @@ router.post(
} }
const effectiveAuthType = authType || authMethod; const effectiveAuthType = authType || authMethod;
const sshDataObj: any = { const sshDataObj: Record<string, unknown> = {
userId: userId, userId: userId,
name, name,
folder: folder || null, folder: folder || null,
@@ -321,11 +322,11 @@ router.post(
enableTerminal: !!createdHost.enableTerminal, enableTerminal: !!createdHost.enableTerminal,
enableTunnel: !!createdHost.enableTunnel, enableTunnel: !!createdHost.enableTunnel,
tunnelConnections: createdHost.tunnelConnections tunnelConnections: createdHost.tunnelConnections
? JSON.parse(createdHost.tunnelConnections) ? JSON.parse(createdHost.tunnelConnections as string)
: [], : [],
enableFileManager: !!createdHost.enableFileManager, enableFileManager: !!createdHost.enableFileManager,
statsConfig: createdHost.statsConfig statsConfig: createdHost.statsConfig
? JSON.parse(createdHost.statsConfig) ? JSON.parse(createdHost.statsConfig as string)
: undefined, : undefined,
}; };
@@ -336,7 +337,7 @@ router.post(
{ {
operation: "host_create_success", operation: "host_create_success",
userId, userId,
hostId: createdHost.id, hostId: createdHost.id as number,
name, name,
ip, ip,
port, port,
@@ -367,8 +368,8 @@ router.put(
upload.single("key"), upload.single("key"),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const hostId = req.params.id; const hostId = req.params.id;
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
let hostData: any; let hostData: Record<string, unknown>;
if (req.headers["content-type"]?.includes("multipart/form-data")) { if (req.headers["content-type"]?.includes("multipart/form-data")) {
if (req.body.data) { if (req.body.data) {
@@ -439,7 +440,7 @@ router.put(
} }
const effectiveAuthType = authType || authMethod; const effectiveAuthType = authType || authMethod;
const sshDataObj: any = { const sshDataObj: Record<string, unknown> = {
name, name,
folder, folder,
tags: Array.isArray(tags) ? tags.join(",") : tags || "", tags: Array.isArray(tags) ? tags.join(",") : tags || "",
@@ -526,11 +527,11 @@ router.put(
enableTerminal: !!updatedHost.enableTerminal, enableTerminal: !!updatedHost.enableTerminal,
enableTunnel: !!updatedHost.enableTunnel, enableTunnel: !!updatedHost.enableTunnel,
tunnelConnections: updatedHost.tunnelConnections tunnelConnections: updatedHost.tunnelConnections
? JSON.parse(updatedHost.tunnelConnections) ? JSON.parse(updatedHost.tunnelConnections as string)
: [], : [],
enableFileManager: !!updatedHost.enableFileManager, enableFileManager: !!updatedHost.enableFileManager,
statsConfig: updatedHost.statsConfig statsConfig: updatedHost.statsConfig
? JSON.parse(updatedHost.statsConfig) ? JSON.parse(updatedHost.statsConfig as string)
: undefined, : undefined,
}; };
@@ -568,7 +569,7 @@ router.put(
// Route: Get SSH data for the authenticated user (requires JWT) // Route: Get SSH data for the authenticated user (requires JWT)
// GET /ssh/host // GET /ssh/host
router.get("/db/host", authenticateJWT, async (req: Request, res: Response) => { router.get("/db/host", authenticateJWT, async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!isNonEmptyString(userId)) { if (!isNonEmptyString(userId)) {
sshLogger.warn("Invalid userId for SSH data fetch", { sshLogger.warn("Invalid userId for SSH data fetch", {
operation: "host_fetch", operation: "host_fetch",
@@ -584,7 +585,7 @@ router.get("/db/host", authenticateJWT, async (req: Request, res: Response) => {
); );
const result = await Promise.all( const result = await Promise.all(
data.map(async (row: any) => { data.map(async (row: Record<string, unknown>) => {
const baseHost = { const baseHost = {
...row, ...row,
tags: tags:
@@ -597,11 +598,11 @@ router.get("/db/host", authenticateJWT, async (req: Request, res: Response) => {
enableTerminal: !!row.enableTerminal, enableTerminal: !!row.enableTerminal,
enableTunnel: !!row.enableTunnel, enableTunnel: !!row.enableTunnel,
tunnelConnections: row.tunnelConnections tunnelConnections: row.tunnelConnections
? JSON.parse(row.tunnelConnections) ? JSON.parse(row.tunnelConnections as string)
: [], : [],
enableFileManager: !!row.enableFileManager, enableFileManager: !!row.enableFileManager,
statsConfig: row.statsConfig statsConfig: row.statsConfig
? JSON.parse(row.statsConfig) ? JSON.parse(row.statsConfig as string)
: undefined, : undefined,
}; };
@@ -626,7 +627,7 @@ router.get(
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const hostId = req.params.id; const hostId = req.params.id;
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!isNonEmptyString(userId) || !hostId) { if (!isNonEmptyString(userId) || !hostId) {
sshLogger.warn("Invalid userId or hostId for SSH host fetch by ID", { sshLogger.warn("Invalid userId or hostId for SSH host fetch by ID", {
@@ -692,7 +693,7 @@ router.get(
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const hostId = req.params.id; const hostId = req.params.id;
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!isNonEmptyString(userId) || !hostId) { if (!isNonEmptyString(userId) || !hostId) {
return res.status(400).json({ error: "Invalid userId or hostId" }); return res.status(400).json({ error: "Invalid userId or hostId" });
@@ -739,7 +740,7 @@ router.get(
enableFileManager: !!resolvedHost.enableFileManager, enableFileManager: !!resolvedHost.enableFileManager,
defaultPath: resolvedHost.defaultPath, defaultPath: resolvedHost.defaultPath,
tunnelConnections: resolvedHost.tunnelConnections tunnelConnections: resolvedHost.tunnelConnections
? JSON.parse(resolvedHost.tunnelConnections) ? JSON.parse(resolvedHost.tunnelConnections as string)
: [], : [],
}; };
@@ -767,7 +768,7 @@ router.delete(
"/db/host/:id", "/db/host/:id",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const hostId = req.params.id; const hostId = req.params.id;
if (!isNonEmptyString(userId) || !hostId) { if (!isNonEmptyString(userId) || !hostId) {
@@ -866,7 +867,7 @@ router.get(
"/file_manager/recent", "/file_manager/recent",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const hostId = req.query.hostId const hostId = req.query.hostId
? parseInt(req.query.hostId as string) ? parseInt(req.query.hostId as string)
: null; : null;
@@ -908,7 +909,7 @@ router.post(
"/file_manager/recent", "/file_manager/recent",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { hostId, path, name } = req.body; const { hostId, path, name } = req.body;
if (!isNonEmptyString(userId) || !hostId || !path) { if (!isNonEmptyString(userId) || !hostId || !path) {
@@ -957,7 +958,7 @@ router.delete(
"/file_manager/recent", "/file_manager/recent",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { hostId, path } = req.body; const { hostId, path } = req.body;
if (!isNonEmptyString(userId) || !hostId || !path) { if (!isNonEmptyString(userId) || !hostId || !path) {
@@ -990,7 +991,7 @@ router.get(
"/file_manager/pinned", "/file_manager/pinned",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const hostId = req.query.hostId const hostId = req.query.hostId
? parseInt(req.query.hostId as string) ? parseInt(req.query.hostId as string)
: null; : null;
@@ -1031,7 +1032,7 @@ router.post(
"/file_manager/pinned", "/file_manager/pinned",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { hostId, path, name } = req.body; const { hostId, path, name } = req.body;
if (!isNonEmptyString(userId) || !hostId || !path) { if (!isNonEmptyString(userId) || !hostId || !path) {
@@ -1077,7 +1078,7 @@ router.delete(
"/file_manager/pinned", "/file_manager/pinned",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { hostId, path } = req.body; const { hostId, path } = req.body;
if (!isNonEmptyString(userId) || !hostId || !path) { if (!isNonEmptyString(userId) || !hostId || !path) {
@@ -1110,7 +1111,7 @@ router.get(
"/file_manager/shortcuts", "/file_manager/shortcuts",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const hostId = req.query.hostId const hostId = req.query.hostId
? parseInt(req.query.hostId as string) ? parseInt(req.query.hostId as string)
: null; : null;
@@ -1151,7 +1152,7 @@ router.post(
"/file_manager/shortcuts", "/file_manager/shortcuts",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { hostId, path, name } = req.body; const { hostId, path, name } = req.body;
if (!isNonEmptyString(userId) || !hostId || !path) { if (!isNonEmptyString(userId) || !hostId || !path) {
@@ -1197,7 +1198,7 @@ router.delete(
"/file_manager/shortcuts", "/file_manager/shortcuts",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { hostId, path } = req.body; const { hostId, path } = req.body;
if (!isNonEmptyString(userId) || !hostId || !path) { if (!isNonEmptyString(userId) || !hostId || !path) {
@@ -1224,21 +1225,26 @@ router.delete(
}, },
); );
async function resolveHostCredentials(host: any): Promise<any> { async function resolveHostCredentials(
host: Record<string, unknown>,
): Promise<Record<string, unknown>> {
try { try {
if (host.credentialId && host.userId) { if (host.credentialId && host.userId) {
const credentialId = host.credentialId as number;
const userId = host.userId as string;
const credentials = await SimpleDBOps.select( const credentials = await SimpleDBOps.select(
db db
.select() .select()
.from(sshCredentials) .from(sshCredentials)
.where( .where(
and( and(
eq(sshCredentials.id, host.credentialId), eq(sshCredentials.id, credentialId),
eq(sshCredentials.userId, host.userId), eq(sshCredentials.userId, userId),
), ),
), ),
"ssh_credentials", "ssh_credentials",
host.userId, userId,
); );
if (credentials.length > 0) { if (credentials.length > 0) {
@@ -1277,7 +1283,7 @@ router.put(
"/folders/rename", "/folders/rename",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { oldName, newName } = req.body; const { oldName, newName } = req.body;
if (!isNonEmptyString(userId) || !oldName || !newName) { if (!isNonEmptyString(userId) || !oldName || !newName) {
@@ -1342,7 +1348,7 @@ router.post(
"/bulk-import", "/bulk-import",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { hosts } = req.body; const { hosts } = req.body;
if (!Array.isArray(hosts) || hosts.length === 0) { if (!Array.isArray(hosts) || hosts.length === 0) {
@@ -1414,7 +1420,7 @@ router.post(
continue; continue;
} }
const sshDataObj: any = { const sshDataObj: Record<string, unknown> = {
userId: userId, userId: userId,
name: hostData.name || `${hostData.username}@${hostData.ip}`, name: hostData.name || `${hostData.username}@${hostData.ip}`,
folder: hostData.folder || "Default", folder: hostData.folder || "Default",
@@ -1472,7 +1478,7 @@ router.post(
authenticateJWT, authenticateJWT,
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { sshConfigId } = req.body; const { sshConfigId } = req.body;
if (!sshConfigId || typeof sshConfigId !== "number") { if (!sshConfigId || typeof sshConfigId !== "number") {
@@ -1536,7 +1542,7 @@ router.post(
const tunnelConnections = JSON.parse(config.tunnelConnections); const tunnelConnections = JSON.parse(config.tunnelConnections);
const resolvedConnections = await Promise.all( const resolvedConnections = await Promise.all(
tunnelConnections.map(async (tunnel: any) => { tunnelConnections.map(async (tunnel: Record<string, unknown>) => {
if ( if (
tunnel.autoStart && tunnel.autoStart &&
tunnel.endpointHost && tunnel.endpointHost &&
@@ -1625,7 +1631,7 @@ router.delete(
"/autostart/disable", "/autostart/disable",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { sshConfigId } = req.body; const { sshConfigId } = req.body;
if (!sshConfigId || typeof sshConfigId !== "number") { if (!sshConfigId || typeof sshConfigId !== "number") {
@@ -1671,7 +1677,7 @@ router.get(
"/autostart/status", "/autostart/status",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
try { try {
const autostartConfigs = await db const autostartConfigs = await db

View File

@@ -1,3 +1,4 @@
import type { AuthenticatedRequest } from "../../../types/index.js";
import express from "express"; import express from "express";
import crypto from "crypto"; import crypto from "crypto";
import { db } from "../db/index.js"; import { db } from "../db/index.js";
@@ -27,7 +28,7 @@ async function verifyOIDCToken(
idToken: string, idToken: string,
issuerUrl: string, issuerUrl: string,
clientId: string, clientId: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
const normalizedIssuerUrl = issuerUrl.endsWith("/") const normalizedIssuerUrl = issuerUrl.endsWith("/")
? issuerUrl.slice(0, -1) ? issuerUrl.slice(0, -1)
: issuerUrl; : issuerUrl;
@@ -48,22 +49,25 @@ async function verifyOIDCToken(
const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`; const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`;
const discoveryResponse = await fetch(discoveryUrl); const discoveryResponse = await fetch(discoveryUrl);
if (discoveryResponse.ok) { if (discoveryResponse.ok) {
const discovery = (await discoveryResponse.json()) as any; const discovery = (await discoveryResponse.json()) as Record<
string,
unknown
>;
if (discovery.jwks_uri) { if (discovery.jwks_uri) {
jwksUrls.unshift(discovery.jwks_uri); jwksUrls.unshift(discovery.jwks_uri as string);
} }
} }
} catch (discoveryError) { } catch (discoveryError) {
authLogger.error(`OIDC discovery failed: ${discoveryError}`); authLogger.error(`OIDC discovery failed: ${discoveryError}`);
} }
let jwks: any = null; let jwks: Record<string, unknown> | null = null;
for (const url of jwksUrls) { for (const url of jwksUrls) {
try { try {
const response = await fetch(url); const response = await fetch(url);
if (response.ok) { if (response.ok) {
const jwksData = (await response.json()) as any; const jwksData = (await response.json()) as Record<string, unknown>;
if (jwksData && jwksData.keys && Array.isArray(jwksData.keys)) { if (jwksData && jwksData.keys && Array.isArray(jwksData.keys)) {
jwks = jwksData; jwks = jwksData;
break; break;
@@ -95,10 +99,12 @@ async function verifyOIDCToken(
); );
const keyId = header.kid; const keyId = header.kid;
const publicKey = jwks.keys.find((key: any) => key.kid === keyId); const publicKey = jwks.keys.find(
(key: Record<string, unknown>) => key.kid === keyId,
);
if (!publicKey) { if (!publicKey) {
throw new Error( throw new Error(
`No matching public key found for key ID: ${keyId}. Available keys: ${jwks.keys.map((k: any) => k.kid).join(", ")}`, `No matching public key found for key ID: ${keyId}. Available keys: ${jwks.keys.map((k: Record<string, unknown>) => k.kid).join(", ")}`,
); );
} }
@@ -115,7 +121,7 @@ async function verifyOIDCToken(
const router = express.Router(); const router = express.Router();
function isNonEmptyString(val: any): val is string { function isNonEmptyString(val: unknown): val is string {
return typeof val === "string" && val.trim().length > 0; return typeof val === "string" && val.trim().length > 0;
} }
@@ -129,7 +135,7 @@ router.post("/create", async (req, res) => {
const row = db.$client const row = db.$client
.prepare("SELECT value FROM settings WHERE key = 'allow_registration'") .prepare("SELECT value FROM settings WHERE key = 'allow_registration'")
.get(); .get();
if (row && (row as any).value !== "true") { if (row && (row as Record<string, unknown>).value !== "true") {
return res return res
.status(403) .status(403)
.json({ error: "Registration is currently disabled" }); .json({ error: "Registration is currently disabled" });
@@ -174,7 +180,7 @@ router.post("/create", async (req, res) => {
const countResult = db.$client const countResult = db.$client
.prepare("SELECT COUNT(*) as count FROM users") .prepare("SELECT COUNT(*) as count FROM users")
.get(); .get();
isFirstUser = ((countResult as any)?.count || 0) === 0; isFirstUser = ((countResult as { count?: number })?.count || 0) === 0;
const saltRounds = parseInt(process.env.SALT || "10", 10); const saltRounds = parseInt(process.env.SALT || "10", 10);
const password_hash = await bcrypt.hash(password, saltRounds); const password_hash = await bcrypt.hash(password, saltRounds);
@@ -238,7 +244,7 @@ router.post("/create", async (req, res) => {
// Route: Create OIDC provider configuration (admin only) // Route: Create OIDC provider configuration (admin only)
// POST /users/oidc-config // POST /users/oidc-config
router.post("/oidc-config", authenticateJWT, async (req, res) => { router.post("/oidc-config", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
try { try {
const user = await db.select().from(users).where(eq(users.id, userId)); const user = await db.select().from(users).where(eq(users.id, userId));
if (!user || user.length === 0 || !user[0].is_admin) { if (!user || user.length === 0 || !user[0].is_admin) {
@@ -378,7 +384,7 @@ router.post("/oidc-config", authenticateJWT, async (req, res) => {
// Route: Disable OIDC configuration (admin only) // Route: Disable OIDC configuration (admin only)
// DELETE /users/oidc-config // DELETE /users/oidc-config
router.delete("/oidc-config", authenticateJWT, async (req, res) => { router.delete("/oidc-config", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
try { try {
const user = await db.select().from(users).where(eq(users.id, userId)); const user = await db.select().from(users).where(eq(users.id, userId));
if (!user || user.length === 0 || !user[0].is_admin) { if (!user || user.length === 0 || !user[0].is_admin) {
@@ -408,7 +414,7 @@ router.get("/oidc-config", async (req, res) => {
return res.json(null); return res.json(null);
} }
let config = JSON.parse((row as any).value); let config = JSON.parse((row as Record<string, unknown>).value as string);
if (config.client_secret) { if (config.client_secret) {
if (config.client_secret.startsWith("encrypted:")) { if (config.client_secret.startsWith("encrypted:")) {
@@ -485,7 +491,7 @@ router.get("/oidc/authorize", async (req, res) => {
return res.status(404).json({ error: "OIDC not configured" }); return res.status(404).json({ error: "OIDC not configured" });
} }
const config = JSON.parse((row as any).value); const config = JSON.parse((row as Record<string, unknown>).value as string);
const state = nanoid(); const state = nanoid();
const nonce = nanoid(); const nonce = nanoid();
@@ -540,7 +546,8 @@ router.get("/oidc/callback", async (req, res) => {
.status(400) .status(400)
.json({ error: "Invalid state parameter - redirect URI not found" }); .json({ error: "Invalid state parameter - redirect URI not found" });
} }
const redirectUri = (storedRedirectRow as any).value; const redirectUri = (storedRedirectRow as Record<string, unknown>)
.value as string;
try { try {
const storedNonce = db.$client const storedNonce = db.$client
@@ -564,7 +571,9 @@ router.get("/oidc/callback", async (req, res) => {
return res.status(500).json({ error: "OIDC not configured" }); return res.status(500).json({ error: "OIDC not configured" });
} }
const config = JSON.parse((configRow as any).value); const config = JSON.parse(
(configRow as Record<string, unknown>).value as string,
);
const tokenResponse = await fetch(config.token_url, { const tokenResponse = await fetch(config.token_url, {
method: "POST", method: "POST",
@@ -590,9 +599,9 @@ router.get("/oidc/callback", async (req, res) => {
.json({ error: "Failed to exchange authorization code" }); .json({ error: "Failed to exchange authorization code" });
} }
const tokenData = (await tokenResponse.json()) as any; const tokenData = (await tokenResponse.json()) as Record<string, unknown>;
let userInfo: any = null; let userInfo: Record<string, unknown> = null;
const userInfoUrls: string[] = []; const userInfoUrls: string[] = [];
const normalizedIssuerUrl = config.issuer_url.endsWith("/") const normalizedIssuerUrl = config.issuer_url.endsWith("/")
@@ -604,9 +613,12 @@ router.get("/oidc/callback", async (req, res) => {
const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`; const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`;
const discoveryResponse = await fetch(discoveryUrl); const discoveryResponse = await fetch(discoveryUrl);
if (discoveryResponse.ok) { if (discoveryResponse.ok) {
const discovery = (await discoveryResponse.json()) as any; const discovery = (await discoveryResponse.json()) as Record<
string,
unknown
>;
if (discovery.userinfo_endpoint) { if (discovery.userinfo_endpoint) {
userInfoUrls.push(discovery.userinfo_endpoint); userInfoUrls.push(discovery.userinfo_endpoint as string);
} }
} }
} catch (discoveryError) { } catch (discoveryError) {
@@ -631,14 +643,14 @@ router.get("/oidc/callback", async (req, res) => {
if (tokenData.id_token) { if (tokenData.id_token) {
try { try {
userInfo = await verifyOIDCToken( userInfo = await verifyOIDCToken(
tokenData.id_token, tokenData.id_token as string,
config.issuer_url, config.issuer_url,
config.client_id, config.client_id,
); );
} catch { } catch {
// Fallback to manual decoding // Fallback to manual decoding
try { try {
const parts = tokenData.id_token.split("."); const parts = (tokenData.id_token as string).split(".");
if (parts.length === 3) { if (parts.length === 3) {
const payload = JSON.parse( const payload = JSON.parse(
Buffer.from(parts[1], "base64").toString(), Buffer.from(parts[1], "base64").toString(),
@@ -661,7 +673,10 @@ router.get("/oidc/callback", async (req, res) => {
}); });
if (userInfoResponse.ok) { if (userInfoResponse.ok) {
userInfo = await userInfoResponse.json(); userInfo = (await userInfoResponse.json()) as Record<
string,
unknown
>;
break; break;
} else { } else {
authLogger.error( authLogger.error(
@@ -684,7 +699,10 @@ router.get("/oidc/callback", async (req, res) => {
return res.status(400).json({ error: "Failed to get user information" }); return res.status(400).json({ error: "Failed to get user information" });
} }
const getNestedValue = (obj: any, path: string): any => { const getNestedValue = (
obj: Record<string, unknown>,
path: string,
): any => {
if (!path || !obj) return null; if (!path || !obj) return null;
return path.split(".").reduce((current, key) => current?.[key], obj); return path.split(".").reduce((current, key) => current?.[key], obj);
}; };
@@ -725,7 +743,7 @@ router.get("/oidc/callback", async (req, res) => {
const countResult = db.$client const countResult = db.$client
.prepare("SELECT COUNT(*) as count FROM users") .prepare("SELECT COUNT(*) as count FROM users")
.get(); .get();
isFirstUser = ((countResult as any)?.count || 0) === 0; isFirstUser = ((countResult as { count?: number })?.count || 0) === 0;
const id = nanoid(); const id = nanoid();
await db.insert(users).values({ await db.insert(users).values({
@@ -787,7 +805,10 @@ router.get("/oidc/callback", async (req, res) => {
expiresIn: "50d", expiresIn: "50d",
}); });
let frontendUrl = redirectUri.replace("/users/oidc/callback", ""); let frontendUrl = (redirectUri as string).replace(
"/users/oidc/callback",
"",
);
if (frontendUrl.includes("localhost")) { if (frontendUrl.includes("localhost")) {
frontendUrl = "http://localhost:5173"; frontendUrl = "http://localhost:5173";
@@ -806,7 +827,10 @@ router.get("/oidc/callback", async (req, res) => {
} catch (err) { } catch (err) {
authLogger.error("OIDC callback failed", err); authLogger.error("OIDC callback failed", err);
let frontendUrl = redirectUri.replace("/users/oidc/callback", ""); let frontendUrl = (redirectUri as string).replace(
"/users/oidc/callback",
"",
);
if (frontendUrl.includes("localhost")) { if (frontendUrl.includes("localhost")) {
frontendUrl = "http://localhost:5173"; frontendUrl = "http://localhost:5173";
@@ -931,7 +955,7 @@ router.post("/login", async (req, res) => {
dataUnlocked: true, dataUnlocked: true,
}); });
const response: any = { const response: Record<string, unknown> = {
success: true, success: true,
is_admin: !!userRecord.is_admin, is_admin: !!userRecord.is_admin,
username: userRecord.username, username: userRecord.username,
@@ -962,7 +986,7 @@ router.post("/login", async (req, res) => {
// POST /users/logout // POST /users/logout
router.post("/logout", async (req, res) => { router.post("/logout", async (req, res) => {
try { try {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (userId) { if (userId) {
authManager.logoutUser(userId); authManager.logoutUser(userId);
@@ -984,7 +1008,7 @@ router.post("/logout", async (req, res) => {
// Route: Get current user's info using JWT // Route: Get current user's info using JWT
// GET /users/me // GET /users/me
router.get("/me", authenticateJWT, async (req: Request, res: Response) => { router.get("/me", authenticateJWT, async (req: Request, res: Response) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!isNonEmptyString(userId)) { if (!isNonEmptyString(userId)) {
authLogger.warn("Invalid userId in JWT for /users/me"); authLogger.warn("Invalid userId in JWT for /users/me");
return res.status(401).json({ error: "Invalid userId" }); return res.status(401).json({ error: "Invalid userId" });
@@ -1019,7 +1043,7 @@ router.get("/setup-required", async (req, res) => {
const countResult = db.$client const countResult = db.$client
.prepare("SELECT COUNT(*) as count FROM users") .prepare("SELECT COUNT(*) as count FROM users")
.get(); .get();
const count = (countResult as any)?.count || 0; const count = (countResult as { count?: number })?.count || 0;
res.json({ res.json({
setup_required: count === 0, setup_required: count === 0,
@@ -1033,7 +1057,7 @@ router.get("/setup-required", async (req, res) => {
// Route: Count users (admin only - for dashboard statistics) // Route: Count users (admin only - for dashboard statistics)
// GET /users/count // GET /users/count
router.get("/count", authenticateJWT, async (req, res) => { router.get("/count", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
try { try {
const user = await db.select().from(users).where(eq(users.id, userId)); const user = await db.select().from(users).where(eq(users.id, userId));
if (!user[0] || !user[0].is_admin) { if (!user[0] || !user[0].is_admin) {
@@ -1043,7 +1067,7 @@ router.get("/count", authenticateJWT, async (req, res) => {
const countResult = db.$client const countResult = db.$client
.prepare("SELECT COUNT(*) as count FROM users") .prepare("SELECT COUNT(*) as count FROM users")
.get(); .get();
const count = (countResult as any)?.count || 0; const count = (countResult as { count?: number })?.count || 0;
res.json({ count }); res.json({ count });
} catch (err) { } catch (err) {
authLogger.error("Failed to count users", err); authLogger.error("Failed to count users", err);
@@ -1070,7 +1094,9 @@ router.get("/registration-allowed", async (req, res) => {
const row = db.$client const row = db.$client
.prepare("SELECT value FROM settings WHERE key = 'allow_registration'") .prepare("SELECT value FROM settings WHERE key = 'allow_registration'")
.get(); .get();
res.json({ allowed: row ? (row as any).value === "true" : true }); res.json({
allowed: row ? (row as Record<string, unknown>).value === "true" : true,
});
} catch (err) { } catch (err) {
authLogger.error("Failed to get registration allowed", err); authLogger.error("Failed to get registration allowed", err);
res.status(500).json({ error: "Failed to get registration allowed" }); res.status(500).json({ error: "Failed to get registration allowed" });
@@ -1080,7 +1106,7 @@ router.get("/registration-allowed", async (req, res) => {
// Route: Set registration allowed status (admin only) // Route: Set registration allowed status (admin only)
// PATCH /users/registration-allowed // PATCH /users/registration-allowed
router.patch("/registration-allowed", authenticateJWT, async (req, res) => { router.patch("/registration-allowed", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
try { try {
const user = await db.select().from(users).where(eq(users.id, userId)); const user = await db.select().from(users).where(eq(users.id, userId));
if (!user || user.length === 0 || !user[0].is_admin) { if (!user || user.length === 0 || !user[0].is_admin) {
@@ -1107,7 +1133,9 @@ router.get("/password-login-allowed", async (req, res) => {
const row = db.$client const row = db.$client
.prepare("SELECT value FROM settings WHERE key = 'allow_password_login'") .prepare("SELECT value FROM settings WHERE key = 'allow_password_login'")
.get(); .get();
res.json({ allowed: row ? (row as { value: string }).value === "true" : true }); res.json({
allowed: row ? (row as { value: string }).value === "true" : true,
});
} catch (err) { } catch (err) {
authLogger.error("Failed to get password login allowed", err); authLogger.error("Failed to get password login allowed", err);
res.status(500).json({ error: "Failed to get password login allowed" }); res.status(500).json({ error: "Failed to get password login allowed" });
@@ -1117,7 +1145,7 @@ router.get("/password-login-allowed", async (req, res) => {
// Route: Set password login allowed status (admin only) // Route: Set password login allowed status (admin only)
// PATCH /users/password-login-allowed // PATCH /users/password-login-allowed
router.patch("/password-login-allowed", authenticateJWT, async (req, res) => { router.patch("/password-login-allowed", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
try { try {
const user = await db.select().from(users).where(eq(users.id, userId)); const user = await db.select().from(users).where(eq(users.id, userId));
if (!user || user.length === 0 || !user[0].is_admin) { if (!user || user.length === 0 || !user[0].is_admin) {
@@ -1128,7 +1156,9 @@ router.patch("/password-login-allowed", authenticateJWT, async (req, res) => {
return res.status(400).json({ error: "Invalid value for allowed" }); return res.status(400).json({ error: "Invalid value for allowed" });
} }
db.$client db.$client
.prepare("UPDATE settings SET value = ? WHERE key = 'allow_password_login'") .prepare(
"UPDATE settings SET value = ? WHERE key = 'allow_password_login'",
)
.run(allowed ? "true" : "false"); .run(allowed ? "true" : "false");
res.json({ allowed }); res.json({ allowed });
} catch (err) { } catch (err) {
@@ -1140,7 +1170,7 @@ router.patch("/password-login-allowed", authenticateJWT, async (req, res) => {
// Route: Delete user account // Route: Delete user account
// DELETE /users/delete-account // DELETE /users/delete-account
router.delete("/delete-account", authenticateJWT, async (req, res) => { router.delete("/delete-account", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { password } = req.body; const { password } = req.body;
if (!isNonEmptyString(password)) { if (!isNonEmptyString(password)) {
@@ -1176,7 +1206,7 @@ router.delete("/delete-account", authenticateJWT, async (req, res) => {
const adminCount = db.$client const adminCount = db.$client
.prepare("SELECT COUNT(*) as count FROM users WHERE is_admin = 1") .prepare("SELECT COUNT(*) as count FROM users WHERE is_admin = 1")
.get(); .get();
if ((adminCount as any)?.count <= 1) { if (((adminCount as { count?: number })?.count || 0) <= 1) {
return res return res
.status(403) .status(403)
.json({ error: "Cannot delete the last admin user" }); .json({ error: "Cannot delete the last admin user" });
@@ -1266,7 +1296,9 @@ router.post("/verify-reset-code", async (req, res) => {
.json({ error: "No reset code found for this user" }); .json({ error: "No reset code found for this user" });
} }
const resetData = JSON.parse((resetDataRow as any).value); const resetData = JSON.parse(
(resetDataRow as Record<string, unknown>).value as string,
);
const now = new Date(); const now = new Date();
const expiresAt = new Date(resetData.expiresAt); const expiresAt = new Date(resetData.expiresAt);
@@ -1324,7 +1356,9 @@ router.post("/complete-reset", async (req, res) => {
return res.status(400).json({ error: "No temporary token found" }); return res.status(400).json({ error: "No temporary token found" });
} }
const tempTokenData = JSON.parse((tempTokenRow as any).value); const tempTokenData = JSON.parse(
(tempTokenRow as Record<string, unknown>).value as string,
);
const now = new Date(); const now = new Date();
const expiresAt = new Date(tempTokenData.expiresAt); const expiresAt = new Date(tempTokenData.expiresAt);
@@ -1412,7 +1446,7 @@ router.post("/complete-reset", async (req, res) => {
// Route: List all users (admin only) // Route: List all users (admin only)
// GET /users/list // GET /users/list
router.get("/list", authenticateJWT, async (req, res) => { router.get("/list", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
try { try {
const user = await db.select().from(users).where(eq(users.id, userId)); const user = await db.select().from(users).where(eq(users.id, userId));
if (!user || user.length === 0 || !user[0].is_admin) { if (!user || user.length === 0 || !user[0].is_admin) {
@@ -1438,7 +1472,7 @@ router.get("/list", authenticateJWT, async (req, res) => {
// Route: Make user admin (admin only) // Route: Make user admin (admin only)
// POST /users/make-admin // POST /users/make-admin
router.post("/make-admin", authenticateJWT, async (req, res) => { router.post("/make-admin", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { username } = req.body; const { username } = req.body;
if (!isNonEmptyString(username)) { if (!isNonEmptyString(username)) {
@@ -1481,7 +1515,7 @@ router.post("/make-admin", authenticateJWT, async (req, res) => {
// Route: Remove admin status (admin only) // Route: Remove admin status (admin only)
// POST /users/remove-admin // POST /users/remove-admin
router.post("/remove-admin", authenticateJWT, async (req, res) => { router.post("/remove-admin", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { username } = req.body; const { username } = req.body;
if (!isNonEmptyString(username)) { if (!isNonEmptyString(username)) {
@@ -1638,7 +1672,7 @@ router.post("/totp/verify-login", async (req, res) => {
}); });
} }
const response: any = { const response: Record<string, unknown> = {
success: true, success: true,
is_admin: !!userRecord.is_admin, is_admin: !!userRecord.is_admin,
username: userRecord.username, username: userRecord.username,
@@ -1668,7 +1702,7 @@ router.post("/totp/verify-login", async (req, res) => {
// Route: Setup TOTP // Route: Setup TOTP
// POST /users/totp/setup // POST /users/totp/setup
router.post("/totp/setup", authenticateJWT, async (req, res) => { router.post("/totp/setup", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
try { try {
const user = await db.select().from(users).where(eq(users.id, userId)); const user = await db.select().from(users).where(eq(users.id, userId));
@@ -1707,7 +1741,7 @@ router.post("/totp/setup", authenticateJWT, async (req, res) => {
// Route: Enable TOTP // Route: Enable TOTP
// POST /users/totp/enable // POST /users/totp/enable
router.post("/totp/enable", authenticateJWT, async (req, res) => { router.post("/totp/enable", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { totp_code } = req.body; const { totp_code } = req.body;
if (!totp_code) { if (!totp_code) {
@@ -1766,7 +1800,7 @@ router.post("/totp/enable", authenticateJWT, async (req, res) => {
// Route: Disable TOTP // Route: Disable TOTP
// POST /users/totp/disable // POST /users/totp/disable
router.post("/totp/disable", authenticateJWT, async (req, res) => { router.post("/totp/disable", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { password, totp_code } = req.body; const { password, totp_code } = req.body;
if (!password && !totp_code) { if (!password && !totp_code) {
@@ -1824,7 +1858,7 @@ router.post("/totp/disable", authenticateJWT, async (req, res) => {
// Route: Generate new backup codes // Route: Generate new backup codes
// POST /users/totp/backup-codes // POST /users/totp/backup-codes
router.post("/totp/backup-codes", authenticateJWT, async (req, res) => { router.post("/totp/backup-codes", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { password, totp_code } = req.body; const { password, totp_code } = req.body;
if (!password && !totp_code) { if (!password && !totp_code) {
@@ -1882,7 +1916,7 @@ router.post("/totp/backup-codes", authenticateJWT, async (req, res) => {
// Route: Delete user (admin only) // Route: Delete user (admin only)
// DELETE /users/delete-user // DELETE /users/delete-user
router.delete("/delete-user", authenticateJWT, async (req, res) => { router.delete("/delete-user", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { username } = req.body; const { username } = req.body;
if (!isNonEmptyString(username)) { if (!isNonEmptyString(username)) {
@@ -1911,7 +1945,7 @@ router.delete("/delete-user", authenticateJWT, async (req, res) => {
const adminCount = db.$client const adminCount = db.$client
.prepare("SELECT COUNT(*) as count FROM users WHERE is_admin = 1") .prepare("SELECT COUNT(*) as count FROM users WHERE is_admin = 1")
.get(); .get();
if ((adminCount as any)?.count <= 1) { if (((adminCount as { count?: number })?.count || 0) <= 1) {
return res return res
.status(403) .status(403)
.json({ error: "Cannot delete the last admin user" }); .json({ error: "Cannot delete the last admin user" });
@@ -1968,7 +2002,7 @@ router.delete("/delete-user", authenticateJWT, async (req, res) => {
// Route: User data unlock - used when session expires // Route: User data unlock - used when session expires
// POST /users/unlock-data // POST /users/unlock-data
router.post("/unlock-data", authenticateJWT, async (req, res) => { router.post("/unlock-data", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { password } = req.body; const { password } = req.body;
if (!password) { if (!password) {
@@ -2001,7 +2035,7 @@ router.post("/unlock-data", authenticateJWT, async (req, res) => {
// Route: Check user data unlock status // Route: Check user data unlock status
// GET /users/data-status // GET /users/data-status
router.get("/data-status", authenticateJWT, async (req, res) => { router.get("/data-status", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
try { try {
const isUnlocked = authManager.isUserUnlocked(userId); const isUnlocked = authManager.isUserUnlocked(userId);
@@ -2023,7 +2057,7 @@ router.get("/data-status", authenticateJWT, async (req, res) => {
// Route: Change user password (re-encrypt data keys) // Route: Change user password (re-encrypt data keys)
// POST /users/change-password // POST /users/change-password
router.post("/change-password", authenticateJWT, async (req, res) => { router.post("/change-password", authenticateJWT, async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
const { currentPassword, newPassword } = req.body; const { currentPassword, newPassword } = req.body;
if (!currentPassword || !newPassword) { if (!currentPassword || !newPassword) {

View File

@@ -9,6 +9,7 @@ import { eq, and } from "drizzle-orm";
import { statsLogger } from "../utils/logger.js"; import { statsLogger } from "../utils/logger.js";
import { SimpleDBOps } from "../utils/simple-db-ops.js"; import { SimpleDBOps } from "../utils/simple-db-ops.js";
import { AuthManager } from "../utils/auth-manager.js"; import { AuthManager } from "../utils/auth-manager.js";
import type { AuthenticatedRequest } from "../../types/index.js";
interface PooledConnection { interface PooledConnection {
client: Client; client: Client;
@@ -237,7 +238,7 @@ class RequestQueue {
} }
interface CachedMetrics { interface CachedMetrics {
data: any; data: unknown;
timestamp: number; timestamp: number;
hostId: number; hostId: number;
} }
@@ -246,7 +247,7 @@ class MetricsCache {
private cache = new Map<number, CachedMetrics>(); private cache = new Map<number, CachedMetrics>();
private ttl = 30000; private ttl = 30000;
get(hostId: number): any | null { get(hostId: number): unknown | null {
const cached = this.cache.get(hostId); const cached = this.cache.get(hostId);
if (cached && Date.now() - cached.timestamp < this.ttl) { if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.data; return cached.data;
@@ -254,7 +255,7 @@ class MetricsCache {
return null; return null;
} }
set(hostId: number, data: any): void { set(hostId: number, data: unknown): void {
this.cache.set(hostId, { this.cache.set(hostId, {
data, data,
timestamp: Date.now(), timestamp: Date.now(),
@@ -297,7 +298,7 @@ interface SSHHostWithCredentials {
enableTunnel: boolean; enableTunnel: boolean;
enableFileManager: boolean; enableFileManager: boolean;
defaultPath: string; defaultPath: string;
tunnelConnections: any[]; tunnelConnections: unknown[];
statsConfig?: string; statsConfig?: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
@@ -432,11 +433,11 @@ async function fetchHostById(
} }
async function resolveHostCredentials( async function resolveHostCredentials(
host: any, host: Record<string, unknown>,
userId: string, userId: string,
): Promise<SSHHostWithCredentials | undefined> { ): Promise<SSHHostWithCredentials | undefined> {
try { try {
const baseHost: any = { const baseHost: Record<string, unknown> = {
id: host.id, id: host.id,
name: host.name, name: host.name,
ip: host.ip, ip: host.ip,
@@ -456,7 +457,7 @@ async function resolveHostCredentials(
enableFileManager: !!host.enableFileManager, enableFileManager: !!host.enableFileManager,
defaultPath: host.defaultPath || "/", defaultPath: host.defaultPath || "/",
tunnelConnections: host.tunnelConnections tunnelConnections: host.tunnelConnections
? JSON.parse(host.tunnelConnections) ? JSON.parse(host.tunnelConnections as string)
: [], : [],
statsConfig: host.statsConfig || undefined, statsConfig: host.statsConfig || undefined,
createdAt: host.createdAt, createdAt: host.createdAt,
@@ -472,7 +473,7 @@ async function resolveHostCredentials(
.from(sshCredentials) .from(sshCredentials)
.where( .where(
and( and(
eq(sshCredentials.id, host.credentialId), eq(sshCredentials.id, host.credentialId as number),
eq(sshCredentials.userId, userId), eq(sshCredentials.userId, userId),
), ),
), ),
@@ -512,7 +513,7 @@ async function resolveHostCredentials(
addLegacyCredentials(baseHost, host); addLegacyCredentials(baseHost, host);
} }
return baseHost; return baseHost as unknown as SSHHostWithCredentials;
} catch (error) { } catch (error) {
statsLogger.error( statsLogger.error(
`Failed to resolve host credentials for host ${host.id}: ${error instanceof Error ? error.message : "Unknown error"}`, `Failed to resolve host credentials for host ${host.id}: ${error instanceof Error ? error.message : "Unknown error"}`,
@@ -521,7 +522,10 @@ async function resolveHostCredentials(
} }
} }
function addLegacyCredentials(baseHost: any, host: any): void { function addLegacyCredentials(
baseHost: Record<string, unknown>,
host: Record<string, unknown>,
): void {
baseHost.password = host.password || null; baseHost.password = host.password || null;
baseHost.key = host.key || null; baseHost.key = host.key || null;
baseHost.keyPassword = host.key_password || host.keyPassword || null; baseHost.keyPassword = host.key_password || host.keyPassword || null;
@@ -573,7 +577,7 @@ function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig {
if (!host.password) { if (!host.password) {
throw new Error(`No password available for host ${host.ip}`); throw new Error(`No password available for host ${host.ip}`);
} }
(base as any).password = host.password; (base as Record<string, unknown>).password = host.password;
} else if (host.authType === "key") { } else if (host.authType === "key") {
if (!host.key) { if (!host.key) {
throw new Error(`No SSH key available for host ${host.ip}`); throw new Error(`No SSH key available for host ${host.ip}`);
@@ -589,10 +593,13 @@ function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig {
.replace(/\r\n/g, "\n") .replace(/\r\n/g, "\n")
.replace(/\r/g, "\n"); .replace(/\r/g, "\n");
(base as any).privateKey = Buffer.from(cleanKey, "utf8"); (base as Record<string, unknown>).privateKey = Buffer.from(
cleanKey,
"utf8",
);
if (host.keyPassword) { if (host.keyPassword) {
(base as any).passphrase = host.keyPassword; (base as Record<string, unknown>).passphrase = host.keyPassword;
} }
} catch (keyError) { } catch (keyError) {
statsLogger.error( statsLogger.error(
@@ -724,7 +731,9 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
}> { }> {
const cached = metricsCache.get(host.id); const cached = metricsCache.get(host.id);
if (cached) { if (cached) {
return cached; return cached as ReturnType<typeof collectMetrics> extends Promise<infer T>
? T
: never;
} }
return requestQueue.queueRequest(host.id, async () => { return requestQueue.queueRequest(host.id, async () => {
@@ -873,7 +882,7 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
} }
// Collect network interfaces // Collect network interfaces
let interfaces: Array<{ const interfaces: Array<{
name: string; name: string;
ip: string; ip: string;
state: string; state: string;
@@ -958,7 +967,7 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
// Collect process information // Collect process information
let totalProcesses: number | null = null; let totalProcesses: number | null = null;
let runningProcesses: number | null = null; let runningProcesses: number | null = null;
let topProcesses: Array<{ const topProcesses: Array<{
pid: string; pid: string;
user: string; user: string;
cpu: string; cpu: string;
@@ -1145,7 +1154,7 @@ async function pollStatusesOnce(userId?: string): Promise<void> {
} }
app.get("/status", async (req, res) => { app.get("/status", async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!SimpleDBOps.isUserDataUnlocked(userId)) { if (!SimpleDBOps.isUserDataUnlocked(userId)) {
return res.status(401).json({ return res.status(401).json({
@@ -1166,7 +1175,7 @@ app.get("/status", async (req, res) => {
app.get("/status/:id", validateHostId, async (req, res) => { app.get("/status/:id", validateHostId, async (req, res) => {
const id = Number(req.params.id); const id = Number(req.params.id);
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!SimpleDBOps.isUserDataUnlocked(userId)) { if (!SimpleDBOps.isUserDataUnlocked(userId)) {
return res.status(401).json({ return res.status(401).json({
@@ -1197,7 +1206,7 @@ app.get("/status/:id", validateHostId, async (req, res) => {
}); });
app.post("/refresh", async (req, res) => { app.post("/refresh", async (req, res) => {
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!SimpleDBOps.isUserDataUnlocked(userId)) { if (!SimpleDBOps.isUserDataUnlocked(userId)) {
return res.status(401).json({ return res.status(401).json({
@@ -1212,7 +1221,7 @@ app.post("/refresh", async (req, res) => {
app.get("/metrics/:id", validateHostId, async (req, res) => { app.get("/metrics/:id", validateHostId, async (req, res) => {
const id = Number(req.params.id); const id = Number(req.params.id);
const userId = (req as any).userId; const userId = (req as AuthenticatedRequest).userId;
if (!SimpleDBOps.isUserDataUnlocked(userId)) { if (!SimpleDBOps.isUserDataUnlocked(userId)) {
return res.status(401).json({ return res.status(401).json({

View File

@@ -403,12 +403,19 @@ wss.on("connection", async (ws: WebSocket, req) => {
if (credentials.length > 0) { if (credentials.length > 0) {
const credential = credentials[0]; const credential = credentials[0];
resolvedCredentials = { resolvedCredentials = {
password: credential.password, password: credential.password as string | undefined,
key: key: (credential.private_key ||
credential.private_key || credential.privateKey || credential.key, credential.privateKey ||
keyPassword: credential.key_password || credential.keyPassword, credential.key) as string | undefined,
keyType: credential.key_type || credential.keyType, keyPassword: (credential.key_password || credential.keyPassword) as
authType: credential.auth_type || credential.authType, | string
| undefined,
keyType: (credential.key_type || credential.keyType) as
| string
| undefined,
authType: (credential.auth_type || credential.authType) as
| string
| undefined,
}; };
} else { } else {
sshLogger.warn(`No credentials found for host ${id}`, { sshLogger.warn(`No credentials found for host ${id}`, {
@@ -617,13 +624,18 @@ wss.on("connection", async (ws: WebSocket, req) => {
); );
} else { } else {
if (resolvedCredentials.password) { if (resolvedCredentials.password) {
const responses = prompts.map(() => resolvedCredentials.password || ""); const responses = prompts.map(
() => resolvedCredentials.password || "",
);
finish(responses); finish(responses);
} else { } else {
sshLogger.warn("Keyboard-interactive requires password but none available", { sshLogger.warn(
operation: "ssh_keyboard_interactive_no_password", "Keyboard-interactive requires password but none available",
hostId: id, {
}); operation: "ssh_keyboard_interactive_no_password",
hostId: id,
},
);
finish(prompts.map(() => "")); finish(prompts.map(() => ""));
} }
} }

View File

@@ -515,12 +515,17 @@ async function connectSSHTunnel(
if (credentials.length > 0) { if (credentials.length > 0) {
const credential = credentials[0]; const credential = credentials[0];
resolvedSourceCredentials = { resolvedSourceCredentials = {
password: credential.password, password: credential.password as string | undefined,
sshKey: sshKey: (credential.private_key ||
credential.private_key || credential.privateKey || credential.key, credential.privateKey ||
keyPassword: credential.key_password || credential.keyPassword, credential.key) as string | undefined,
keyType: credential.key_type || credential.keyType, keyPassword: (credential.key_password || credential.keyPassword) as
authMethod: credential.auth_type || credential.authType, | string
| undefined,
keyType: (credential.key_type || credential.keyType) as
| string
| undefined,
authMethod: (credential.auth_type || credential.authType) as string,
}; };
} }
} }
@@ -593,12 +598,17 @@ async function connectSSHTunnel(
if (credentials.length > 0) { if (credentials.length > 0) {
const credential = credentials[0]; const credential = credentials[0];
resolvedEndpointCredentials = { resolvedEndpointCredentials = {
password: credential.password, password: credential.password as string | undefined,
sshKey: sshKey: (credential.private_key ||
credential.private_key || credential.privateKey || credential.key, credential.privateKey ||
keyPassword: credential.key_password || credential.keyPassword, credential.key) as string | undefined,
keyType: credential.key_type || credential.keyType, keyPassword: (credential.key_password || credential.keyPassword) as
authMethod: credential.auth_type || credential.authType, | string
| undefined,
keyType: (credential.key_type || credential.keyType) as
| string
| undefined,
authMethod: (credential.auth_type || credential.authType) as string,
}; };
} else { } else {
tunnelLogger.warn("No endpoint credentials found in database", { tunnelLogger.warn("No endpoint credentials found in database", {
@@ -1031,12 +1041,17 @@ async function killRemoteTunnelByMarker(
if (credentials.length > 0) { if (credentials.length > 0) {
const credential = credentials[0]; const credential = credentials[0];
resolvedSourceCredentials = { resolvedSourceCredentials = {
password: credential.password, password: credential.password as string | undefined,
sshKey: sshKey: (credential.private_key ||
credential.private_key || credential.privateKey || credential.key, credential.privateKey ||
keyPassword: credential.key_password || credential.keyPassword, credential.key) as string | undefined,
keyType: credential.key_type || credential.keyType, keyPassword: (credential.key_password || credential.keyPassword) as
authMethod: credential.auth_type || credential.authType, | string
| undefined,
keyType: (credential.key_type || credential.keyType) as
| string
| undefined,
authMethod: (credential.auth_type || credential.authType) as string,
}; };
} }
} }

View File

@@ -12,7 +12,7 @@ class DataCrypto {
static encryptRecord( static encryptRecord(
tableName: string, tableName: string,
record: any, record: Record<string, unknown>,
userId: string, userId: string,
userDataKey: Buffer, userDataKey: Buffer,
): any { ): any {
@@ -24,7 +24,7 @@ class DataCrypto {
encryptedRecord[fieldName] = FieldCrypto.encryptField( encryptedRecord[fieldName] = FieldCrypto.encryptField(
value as string, value as string,
userDataKey, userDataKey,
recordId, recordId as string,
fieldName, fieldName,
); );
} }
@@ -35,7 +35,7 @@ class DataCrypto {
static decryptRecord( static decryptRecord(
tableName: string, tableName: string,
record: any, record: Record<string, unknown>,
userId: string, userId: string,
userDataKey: Buffer, userDataKey: Buffer,
): any { ): any {
@@ -49,7 +49,7 @@ class DataCrypto {
decryptedRecord[fieldName] = LazyFieldEncryption.safeGetFieldValue( decryptedRecord[fieldName] = LazyFieldEncryption.safeGetFieldValue(
value as string, value as string,
userDataKey, userDataKey,
recordId, recordId as string,
fieldName, fieldName,
); );
} }
@@ -60,13 +60,18 @@ class DataCrypto {
static decryptRecords( static decryptRecords(
tableName: string, tableName: string,
records: any[], records: unknown[],
userId: string, userId: string,
userDataKey: Buffer, userDataKey: Buffer,
): any[] { ): unknown[] {
if (!Array.isArray(records)) return records; if (!Array.isArray(records)) return records;
return records.map((record) => return records.map((record) =>
this.decryptRecord(tableName, record, userId, userDataKey), this.decryptRecord(
tableName,
record as Record<string, unknown>,
userId,
userDataKey,
),
); );
} }
@@ -386,7 +391,7 @@ class DataCrypto {
static encryptRecordForUser( static encryptRecordForUser(
tableName: string, tableName: string,
record: any, record: Record<string, unknown>,
userId: string, userId: string,
): any { ): any {
const userDataKey = this.validateUserAccess(userId); const userDataKey = this.validateUserAccess(userId);
@@ -395,7 +400,7 @@ class DataCrypto {
static decryptRecordForUser( static decryptRecordForUser(
tableName: string, tableName: string,
record: any, record: Record<string, unknown>,
userId: string, userId: string,
): any { ): any {
const userDataKey = this.validateUserAccess(userId); const userDataKey = this.validateUserAccess(userId);
@@ -404,9 +409,9 @@ class DataCrypto {
static decryptRecordsForUser( static decryptRecordsForUser(
tableName: string, tableName: string,
records: any[], records: unknown[],
userId: string, userId: string,
): any[] { ): unknown[] {
const userDataKey = this.validateUserAccess(userId); const userDataKey = this.validateUserAccess(userId);
return this.decryptRecords(tableName, records, userId, userDataKey); return this.decryptRecords(tableName, records, userId, userDataKey);
} }

View File

@@ -5,7 +5,8 @@ import type { SQLiteTable } from "drizzle-orm/sqlite-core";
type TableName = "users" | "ssh_data" | "ssh_credentials"; type TableName = "users" | "ssh_data" | "ssh_credentials";
class SimpleDBOps { class SimpleDBOps {
static async insert<T extends Record<string, any>>( static async insert<T extends Record<string, unknown>>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
table: SQLiteTable<any>, table: SQLiteTable<any>,
tableName: TableName, tableName: TableName,
data: T, data: T,
@@ -44,8 +45,8 @@ class SimpleDBOps {
return decryptedResult as T; return decryptedResult as T;
} }
static async select<T extends Record<string, any>>( static async select<T extends Record<string, unknown>>(
query: any, query: unknown,
tableName: TableName, tableName: TableName,
userId: string, userId: string,
): Promise<T[]> { ): Promise<T[]> {
@@ -58,16 +59,16 @@ class SimpleDBOps {
const decryptedResults = DataCrypto.decryptRecords( const decryptedResults = DataCrypto.decryptRecords(
tableName, tableName,
results, results as unknown[],
userId, userId,
userDataKey, userDataKey,
); );
return decryptedResults; return decryptedResults as T[];
} }
static async selectOne<T extends Record<string, any>>( static async selectOne<T extends Record<string, unknown>>(
query: any, query: unknown,
tableName: TableName, tableName: TableName,
userId: string, userId: string,
): Promise<T | undefined> { ): Promise<T | undefined> {
@@ -81,7 +82,7 @@ class SimpleDBOps {
const decryptedResult = DataCrypto.decryptRecord( const decryptedResult = DataCrypto.decryptRecord(
tableName, tableName,
result, result as Record<string, unknown>,
userId, userId,
userDataKey, userDataKey,
); );
@@ -89,10 +90,11 @@ class SimpleDBOps {
return decryptedResult; return decryptedResult;
} }
static async update<T extends Record<string, any>>( static async update<T extends Record<string, unknown>>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
table: SQLiteTable<any>, table: SQLiteTable<any>,
tableName: TableName, tableName: TableName,
where: any, where: unknown,
data: Partial<T>, data: Partial<T>,
userId: string, userId: string,
): Promise<T[]> { ): Promise<T[]> {
@@ -108,7 +110,8 @@ class SimpleDBOps {
const result = await getDb() const result = await getDb()
.update(table) .update(table)
.set(encryptedData) .set(encryptedData)
.where(where) // eslint-disable-next-line @typescript-eslint/no-explicit-any
.where(where as any)
.returning(); .returning();
DatabaseSaveTrigger.triggerSave(`update_${tableName}`); DatabaseSaveTrigger.triggerSave(`update_${tableName}`);
@@ -124,12 +127,17 @@ class SimpleDBOps {
} }
static async delete( static async delete(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
table: SQLiteTable<any>, table: SQLiteTable<any>,
tableName: TableName, tableName: TableName,
where: any, where: unknown,
_userId: string, _userId: string,
): Promise<any[]> { ): Promise<unknown[]> {
const result = await getDb().delete(table).where(where).returning(); // eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await getDb()
.delete(table)
.where(where as any)
.returning();
DatabaseSaveTrigger.triggerSave(`delete_${tableName}`); DatabaseSaveTrigger.triggerSave(`delete_${tableName}`);
@@ -145,12 +153,12 @@ class SimpleDBOps {
} }
static async selectEncrypted( static async selectEncrypted(
query: any, query: unknown,
_tableName: TableName, _tableName: TableName,
): Promise<any[]> { ): Promise<unknown[]> {
const results = await query; const results = await query;
return results; return results as unknown[];
} }
} }

View File

@@ -18,14 +18,14 @@ interface UserExportData {
userId: string; userId: string;
username: string; username: string;
userData: { userData: {
sshHosts: any[]; sshHosts: unknown[];
sshCredentials: any[]; sshCredentials: unknown[];
fileManagerData: { fileManagerData: {
recent: any[]; recent: unknown[];
pinned: any[]; pinned: unknown[];
shortcuts: any[]; shortcuts: unknown[];
}; };
dismissedAlerts: any[]; dismissedAlerts: unknown[];
}; };
metadata: { metadata: {
totalRecords: number; totalRecords: number;
@@ -83,7 +83,7 @@ class UserDataExport {
) )
: sshHosts; : sshHosts;
let sshCredentialsData: any[] = []; let sshCredentialsData: unknown[] = [];
if (includeCredentials) { if (includeCredentials) {
const credentials = await getDb() const credentials = await getDb()
.select() .select()
@@ -185,7 +185,10 @@ class UserDataExport {
return JSON.stringify(exportData, null, pretty ? 2 : 0); return JSON.stringify(exportData, null, pretty ? 2 : 0);
} }
static validateExportData(data: any): { valid: boolean; errors: string[] } { static validateExportData(data: unknown): {
valid: boolean;
errors: string[];
} {
const errors: string[] = []; const errors: string[] = [];
if (!data || typeof data !== "object") { if (!data || typeof data !== "object") {
@@ -193,23 +196,26 @@ class UserDataExport {
return { valid: false, errors }; return { valid: false, errors };
} }
if (!data.version) { const dataObj = data as Record<string, unknown>;
if (!dataObj.version) {
errors.push("Missing version field"); errors.push("Missing version field");
} }
if (!data.userId) { if (!dataObj.userId) {
errors.push("Missing userId field"); errors.push("Missing userId field");
} }
if (!data.userData || typeof data.userData !== "object") { if (!dataObj.userData || typeof dataObj.userData !== "object") {
errors.push("Missing or invalid userData field"); errors.push("Missing or invalid userData field");
} }
if (!data.metadata || typeof data.metadata !== "object") { if (!dataObj.metadata || typeof dataObj.metadata !== "object") {
errors.push("Missing or invalid metadata field"); errors.push("Missing or invalid metadata field");
} }
if (data.userData) { if (dataObj.userData) {
const userData = dataObj.userData as Record<string, unknown>;
const requiredFields = [ const requiredFields = [
"sshHosts", "sshHosts",
"sshCredentials", "sshCredentials",
@@ -218,23 +224,24 @@ class UserDataExport {
]; ];
for (const field of requiredFields) { for (const field of requiredFields) {
if ( if (
!Array.isArray(data.userData[field]) && !Array.isArray(userData[field]) &&
!( !(field === "fileManagerData" && typeof userData[field] === "object")
field === "fileManagerData" &&
typeof data.userData[field] === "object"
)
) { ) {
errors.push(`Missing or invalid userData.${field} field`); errors.push(`Missing or invalid userData.${field} field`);
} }
} }
if ( if (
data.userData.fileManagerData && userData.fileManagerData &&
typeof data.userData.fileManagerData === "object" typeof userData.fileManagerData === "object"
) { ) {
const fileManagerData = userData.fileManagerData as Record<
string,
unknown
>;
const fmFields = ["recent", "pinned", "shortcuts"]; const fmFields = ["recent", "pinned", "shortcuts"];
for (const field of fmFields) { for (const field of fmFields) {
if (!Array.isArray(data.userData.fileManagerData[field])) { if (!Array.isArray(fileManagerData[field])) {
errors.push( errors.push(
`Missing or invalid userData.fileManagerData.${field} field`, `Missing or invalid userData.fileManagerData.${field} field`,
); );

View File

@@ -1,22 +1,49 @@
interface ServerConfig {
serverUrl?: string;
[key: string]: unknown;
}
interface ConnectionTestResult {
success: boolean;
error?: string;
[key: string]: unknown;
}
interface DialogOptions {
title?: string;
defaultPath?: string;
buttonLabel?: string;
filters?: Array<{ name: string; extensions: string[] }>;
properties?: string[];
[key: string]: unknown;
}
interface DialogResult {
canceled: boolean;
filePath?: string;
filePaths?: string[];
[key: string]: unknown;
}
export interface ElectronAPI { export interface ElectronAPI {
getAppVersion: () => Promise<string>; getAppVersion: () => Promise<string>;
getPlatform: () => Promise<string>; getPlatform: () => Promise<string>;
getServerConfig: () => Promise<any>; getServerConfig: () => Promise<ServerConfig>;
saveServerConfig: (config: any) => Promise<any>; saveServerConfig: (config: ServerConfig) => Promise<{ success: boolean }>;
testServerConnection: (serverUrl: string) => Promise<any>; testServerConnection: (serverUrl: string) => Promise<ConnectionTestResult>;
showSaveDialog: (options: any) => Promise<any>; showSaveDialog: (options: DialogOptions) => Promise<DialogResult>;
showOpenDialog: (options: any) => Promise<any>; showOpenDialog: (options: DialogOptions) => Promise<DialogResult>;
onUpdateAvailable: (callback: Function) => void; onUpdateAvailable: (callback: () => void) => void;
onUpdateDownloaded: (callback: Function) => void; onUpdateDownloaded: (callback: () => void) => void;
removeAllListeners: (channel: string) => void; removeAllListeners: (channel: string) => void;
isElectron: boolean; isElectron: boolean;
isDev: boolean; isDev: boolean;
invoke: (channel: string, ...args: any[]) => Promise<any>; invoke: (channel: string, ...args: unknown[]) => Promise<unknown>;
createTempFile: (fileData: { createTempFile: (fileData: {
fileName: string; fileName: string;

View File

@@ -4,6 +4,7 @@
// This file contains all shared interfaces and types used across the application // This file contains all shared interfaces and types used across the application
import type { Client } from "ssh2"; import type { Client } from "ssh2";
import type { Request } from "express";
// ============================================================================ // ============================================================================
// SSH HOST TYPES // SSH HOST TYPES
@@ -58,7 +59,7 @@ export interface SSHHostData {
enableTunnel?: boolean; enableTunnel?: boolean;
enableFileManager?: boolean; enableFileManager?: boolean;
defaultPath?: string; defaultPath?: string;
tunnelConnections?: any[]; tunnelConnections?: TunnelConnection[];
statsConfig?: string; statsConfig?: string;
} }
@@ -263,8 +264,8 @@ export interface TabContextTab {
| "file_manager" | "file_manager"
| "user_profile"; | "user_profile";
title: string; title: string;
hostConfig?: any; hostConfig?: SSHHost;
terminalRef?: React.RefObject<any>; terminalRef?: React.RefObject<HTMLElement>;
} }
// ============================================================================ // ============================================================================
@@ -305,7 +306,7 @@ export type KeyType = "rsa" | "ecdsa" | "ed25519";
// API RESPONSE TYPES // API RESPONSE TYPES
// ============================================================================ // ============================================================================
export interface ApiResponse<T = any> { export interface ApiResponse<T = unknown> {
data?: T; data?: T;
error?: string; error?: string;
message?: string; message?: string;
@@ -368,13 +369,13 @@ export interface SSHTunnelViewerProps {
action: "connect" | "disconnect" | "cancel", action: "connect" | "disconnect" | "cancel",
host: SSHHost, host: SSHHost,
tunnelIndex: number, tunnelIndex: number,
) => Promise<any> ) => Promise<void>
>; >;
onTunnelAction?: ( onTunnelAction?: (
action: "connect" | "disconnect" | "cancel", action: "connect" | "disconnect" | "cancel",
host: SSHHost, host: SSHHost,
tunnelIndex: number, tunnelIndex: number,
) => Promise<any>; ) => Promise<void>;
} }
export interface FileManagerProps { export interface FileManagerProps {
@@ -402,7 +403,7 @@ export interface SSHTunnelObjectProps {
action: "connect" | "disconnect" | "cancel", action: "connect" | "disconnect" | "cancel",
host: SSHHost, host: SSHHost,
tunnelIndex: number, tunnelIndex: number,
) => Promise<any>; ) => Promise<void>;
compact?: boolean; compact?: boolean;
bare?: boolean; bare?: boolean;
} }
@@ -461,3 +462,95 @@ export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
export type RequiredFields<T, K extends keyof T> = T & Required<Pick<T, K>>; export type RequiredFields<T, K extends keyof T> = T & Required<Pick<T, K>>;
export type PartialExcept<T, K extends keyof T> = Partial<T> & Pick<T, K>; export type PartialExcept<T, K extends keyof T> = Partial<T> & Pick<T, K>;
// ============================================================================
// EXPRESS REQUEST TYPES
// ============================================================================
export interface AuthenticatedRequest extends Request {
userId: string;
user?: {
id: string;
username: string;
isAdmin: boolean;
};
}
// ============================================================================
// GITHUB API TYPES
// ============================================================================
export interface GitHubAsset {
id: number;
name: string;
size: number;
download_count: number;
browser_download_url: string;
}
export interface GitHubRelease {
id: number;
tag_name: string;
name: string;
body: string;
published_at: string;
html_url: string;
assets: GitHubAsset[];
prerelease: boolean;
draft: boolean;
}
export interface GitHubAPIResponse<T> {
data: T;
cached: boolean;
cache_age?: number;
timestamp?: number;
}
// ============================================================================
// CACHE TYPES
// ============================================================================
export interface CacheEntry<T = unknown> {
data: T;
timestamp: number;
expiresAt: number;
}
// ============================================================================
// DATABASE EXPORT/IMPORT TYPES
// ============================================================================
export interface ExportSummary {
sshHostsImported: number;
sshCredentialsImported: number;
fileManagerItemsImported: number;
dismissedAlertsImported: number;
credentialUsageImported: number;
settingsImported: number;
skippedItems: number;
errors: string[];
}
export interface ImportResult {
success: boolean;
summary: ExportSummary;
}
export interface ExportRequestBody {
password: string;
}
export interface ImportRequestBody {
password: string;
}
export interface ExportPreviewBody {
scope?: string;
includeCredentials?: boolean;
}
export interface RestoreRequestBody {
backupPath: string;
targetPath?: string;
}

View File

@@ -107,7 +107,8 @@ export function AdminSettings({
React.useEffect(() => { React.useEffect(() => {
if (isElectron()) { if (isElectron()) {
const serverUrl = (window as any).configuredServerUrl; const serverUrl = (window as { configuredServerUrl?: string })
.configuredServerUrl;
if (!serverUrl) { if (!serverUrl) {
return; return;
} }
@@ -127,7 +128,8 @@ export function AdminSettings({
React.useEffect(() => { React.useEffect(() => {
if (isElectron()) { if (isElectron()) {
const serverUrl = (window as any).configuredServerUrl; const serverUrl = (window as { configuredServerUrl?: string })
.configuredServerUrl;
if (!serverUrl) { if (!serverUrl) {
return; return;
} }
@@ -148,7 +150,8 @@ export function AdminSettings({
React.useEffect(() => { React.useEffect(() => {
if (isElectron()) { if (isElectron()) {
const serverUrl = (window as any).configuredServerUrl; const serverUrl = (window as { configuredServerUrl?: string })
.configuredServerUrl;
if (!serverUrl) { if (!serverUrl) {
return; return;
} }
@@ -169,7 +172,8 @@ export function AdminSettings({
const fetchUsers = async () => { const fetchUsers = async () => {
if (isElectron()) { if (isElectron()) {
const serverUrl = (window as any).configuredServerUrl; const serverUrl = (window as { configuredServerUrl?: string })
.configuredServerUrl;
if (!serverUrl) { if (!serverUrl) {
return; return;
} }
@@ -234,9 +238,10 @@ export function AdminSettings({
try { try {
await updateOIDCConfig(oidcConfig); await updateOIDCConfig(oidcConfig);
toast.success(t("admin.oidcConfigurationUpdated")); toast.success(t("admin.oidcConfigurationUpdated"));
} catch (err: any) { } catch (err: unknown) {
setOidcError( setOidcError(
err?.response?.data?.error || t("admin.failedToUpdateOidcConfig"), (err as { response?: { data?: { error?: string } } })?.response?.data
?.error || t("admin.failedToUpdateOidcConfig"),
); );
} finally { } finally {
setOidcLoading(false); setOidcLoading(false);
@@ -257,9 +262,10 @@ export function AdminSettings({
toast.success(t("admin.userIsNowAdmin", { username: newAdminUsername })); toast.success(t("admin.userIsNowAdmin", { username: newAdminUsername }));
setNewAdminUsername(""); setNewAdminUsername("");
fetchUsers(); fetchUsers();
} catch (err: any) { } catch (err: unknown) {
setMakeAdminError( setMakeAdminError(
err?.response?.data?.error || t("admin.failedToMakeUserAdmin"), (err as { response?: { data?: { error?: string } } })?.response?.data
?.error || t("admin.failedToMakeUserAdmin"),
); );
} finally { } finally {
setMakeAdminLoading(false); setMakeAdminLoading(false);
@@ -272,7 +278,7 @@ export function AdminSettings({
await removeAdminStatus(username); await removeAdminStatus(username);
toast.success(t("admin.adminStatusRemoved", { username })); toast.success(t("admin.adminStatusRemoved", { username }));
fetchUsers(); fetchUsers();
} catch (err: any) { } catch (err: unknown) {
toast.error(t("admin.failedToRemoveAdminStatus")); toast.error(t("admin.failedToRemoveAdminStatus"));
} }
}); });
@@ -286,7 +292,7 @@ export function AdminSettings({
await deleteUser(username); await deleteUser(username);
toast.success(t("admin.userDeletedSuccessfully", { username })); toast.success(t("admin.userDeletedSuccessfully", { username }));
fetchUsers(); fetchUsers();
} catch (err: any) { } catch (err: unknown) {
toast.error(t("admin.failedToDeleteUser")); toast.error(t("admin.failedToDeleteUser"));
} }
}, },
@@ -316,7 +322,7 @@ export function AdminSettings({
window.location.hostname === "127.0.0.1"); window.location.hostname === "127.0.0.1");
const apiUrl = isElectron() const apiUrl = isElectron()
? `${(window as any).configuredServerUrl}/database/export` ? `${(window as { configuredServerUrl?: string }).configuredServerUrl}/database/export`
: isDev : isDev
? `http://localhost:30001/database/export` ? `http://localhost:30001/database/export`
: `${window.location.protocol}//${window.location.host}/database/export`; : `${window.location.protocol}//${window.location.host}/database/export`;
@@ -386,7 +392,7 @@ export function AdminSettings({
window.location.hostname === "127.0.0.1"); window.location.hostname === "127.0.0.1");
const apiUrl = isElectron() const apiUrl = isElectron()
? `${(window as any).configuredServerUrl}/database/import` ? `${(window as { configuredServerUrl?: string }).configuredServerUrl}/database/import`
: isDev : isDev
? `http://localhost:30001/database/import` ? `http://localhost:30001/database/import`
: `${window.location.protocol}//${window.location.host}/database/import`; : `${window.location.protocol}//${window.location.host}/database/import`;
@@ -713,9 +719,13 @@ export function AdminSettings({
try { try {
await disableOIDCConfig(); await disableOIDCConfig();
toast.success(t("admin.oidcConfigurationDisabled")); toast.success(t("admin.oidcConfigurationDisabled"));
} catch (err: any) { } catch (err: unknown) {
setOidcError( setOidcError(
err?.response?.data?.error || (
err as {
response?: { data?: { error?: string } };
}
)?.response?.data?.error ||
t("admin.failedToDisableOidcConfig"), t("admin.failedToDisableOidcConfig"),
); );
} finally { } finally {

View File

@@ -311,10 +311,10 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
setFiles(files); setFiles(files);
clearSelection(); clearSelection();
initialLoadDoneRef.current = true; initialLoadDoneRef.current = true;
} catch (dirError: any) { } catch (dirError: unknown) {
console.error("Failed to load initial directory:", dirError); console.error("Failed to load initial directory:", dirError);
} }
} catch (error: any) { } catch (error: unknown) {
console.error("SSH connection failed:", error); console.error("SSH connection failed:", error);
handleCloseWithError( handleCloseWithError(
t("fileManager.failedToConnect") + ": " + (error.message || error), t("fileManager.failedToConnect") + ": " + (error.message || error),
@@ -353,7 +353,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
setFiles(files); setFiles(files);
clearSelection(); clearSelection();
} catch (error: any) { } catch (error: unknown) {
if (currentLoadingPathRef.current === path) { if (currentLoadingPathRef.current === path) {
console.error("Failed to load directory:", error); console.error("Failed to load directory:", error);
@@ -535,7 +535,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
t("fileManager.fileUploadedSuccessfully", { name: file.name }), t("fileManager.fileUploadedSuccessfully", { name: file.name }),
); );
handleRefreshDirectory(); handleRefreshDirectory();
} catch (error: any) { } catch (error: unknown) {
toast.dismiss(progressToast); toast.dismiss(progressToast);
if ( if (
@@ -584,7 +584,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
t("fileManager.fileDownloadedSuccessfully", { name: file.name }), t("fileManager.fileDownloadedSuccessfully", { name: file.name }),
); );
} }
} catch (error: any) { } catch (error: unknown) {
if ( if (
error.message?.includes("connection") || error.message?.includes("connection") ||
error.message?.includes("established") error.message?.includes("established")
@@ -665,7 +665,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
); );
handleRefreshDirectory(); handleRefreshDirectory();
clearSelection(); clearSelection();
} catch (error: any) { } catch (error: unknown) {
if ( if (
error.message?.includes("connection") || error.message?.includes("connection") ||
error.message?.includes("established") error.message?.includes("established")
@@ -775,7 +775,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
component: createWindowComponent, component: createWindowComponent,
}); });
} }
} catch (error: any) { } catch (error: unknown) {
toast.error( toast.error(
error?.response?.data?.error || error?.response?.data?.error ||
error?.message || error?.message ||
@@ -914,7 +914,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
successCount++; successCount++;
} }
} }
} catch (error: any) { } catch (error: unknown) {
console.error(`Failed to ${operation} file ${file.name}:`, error); console.error(`Failed to ${operation} file ${file.name}:`, error);
toast.error( toast.error(
t("fileManager.operationFailed", { t("fileManager.operationFailed", {
@@ -1015,7 +1015,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
if (operation === "cut") { if (operation === "cut") {
setClipboard(null); setClipboard(null);
} }
} catch (error: any) { } catch (error: unknown) {
toast.error( toast.error(
`${t("fileManager.pasteFailed")}: ${error.message || t("fileManager.unknownError")}`, `${t("fileManager.pasteFailed")}: ${error.message || t("fileManager.unknownError")}`,
); );
@@ -1050,7 +1050,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
currentHost?.userId?.toString(), currentHost?.userId?.toString(),
); );
successCount++; successCount++;
} catch (error: any) { } catch (error: unknown) {
console.error( console.error(
`Failed to delete copied file ${copiedFile.targetName}:`, `Failed to delete copied file ${copiedFile.targetName}:`,
error, error,
@@ -1092,7 +1092,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
currentHost?.userId?.toString(), currentHost?.userId?.toString(),
); );
successCount++; successCount++;
} catch (error: any) { } catch (error: unknown) {
console.error( console.error(
`Failed to move back file ${movedFile.targetName}:`, `Failed to move back file ${movedFile.targetName}:`,
error, error,
@@ -1132,7 +1132,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
} }
handleRefreshDirectory(); handleRefreshDirectory();
} catch (error: any) { } catch (error: unknown) {
toast.error( toast.error(
`${t("fileManager.undoOperationFailed")}: ${error.message || t("fileManager.unknownError")}`, `${t("fileManager.undoOperationFailed")}: ${error.message || t("fileManager.unknownError")}`,
); );
@@ -1204,7 +1204,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
setCreateIntent(null); setCreateIntent(null);
handleRefreshDirectory(); handleRefreshDirectory();
} catch (error: any) { } catch (error: unknown) {
console.error("Create failed:", error); console.error("Create failed:", error);
toast.error(t("fileManager.failedToCreateItem")); toast.error(t("fileManager.failedToCreateItem"));
} }
@@ -1233,7 +1233,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
); );
setEditingFile(null); setEditingFile(null);
handleRefreshDirectory(); handleRefreshDirectory();
} catch (error: any) { } catch (error: unknown) {
console.error("Rename failed:", error); console.error("Rename failed:", error);
toast.error(t("fileManager.failedToRenameItem")); toast.error(t("fileManager.failedToRenameItem"));
} }
@@ -1269,11 +1269,11 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
clearSelection(); clearSelection();
initialLoadDoneRef.current = true; initialLoadDoneRef.current = true;
toast.success(t("fileManager.connectedSuccessfully")); toast.success(t("fileManager.connectedSuccessfully"));
} catch (dirError: any) { } catch (dirError: unknown) {
console.error("Failed to load initial directory:", dirError); console.error("Failed to load initial directory:", dirError);
} }
} }
} catch (error: any) { } catch (error: unknown) {
console.error("TOTP verification failed:", error); console.error("TOTP verification failed:", error);
toast.error(t("fileManager.totpVerificationFailed")); toast.error(t("fileManager.totpVerificationFailed"));
} finally { } finally {
@@ -1340,7 +1340,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
movedItems.push(file.name); movedItems.push(file.name);
successCount++; successCount++;
} }
} catch (error: any) { } catch (error: unknown) {
console.error(`Failed to move file ${file.name}:`, error); console.error(`Failed to move file ${file.name}:`, error);
toast.error( toast.error(
t("fileManager.moveFileFailed", { name: file.name }) + t("fileManager.moveFileFailed", { name: file.name }) +
@@ -1388,7 +1388,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
handleRefreshDirectory(); handleRefreshDirectory();
clearSelection(); clearSelection();
} }
} catch (error: any) { } catch (error: unknown) {
console.error("Drag move operation failed:", error); console.error("Drag move operation failed:", error);
toast.error(t("fileManager.moveOperationFailed") + ": " + error.message); toast.error(t("fileManager.moveOperationFailed") + ": " + error.message);
} }
@@ -1459,7 +1459,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
await dragToDesktop.dragFilesToDesktop(files); await dragToDesktop.dragFilesToDesktop(files);
} }
} }
} catch (error: any) { } catch (error: unknown) {
console.error("Drag to desktop failed:", error); console.error("Drag to desktop failed:", error);
toast.error( toast.error(
t("fileManager.dragFailed") + t("fileManager.dragFailed") +
@@ -1554,7 +1554,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
try { try {
const pinnedData = await getPinnedFiles(currentHost.id); const pinnedData = await getPinnedFiles(currentHost.id);
const pinnedPaths = new Set(pinnedData.map((item: any) => item.path)); const pinnedPaths = new Set(
pinnedData.map((item: Record<string, unknown>) => item.path),
);
setPinnedFiles(pinnedPaths); setPinnedFiles(pinnedPaths);
} catch (error) { } catch (error) {
console.error("Failed to load pinned files:", error); console.error("Failed to load pinned files:", error);

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,21 @@ import {
} from "lucide-react"; } from "lucide-react";
import { Button } from "@/components/ui/button.tsx"; import { Button } from "@/components/ui/button.tsx";
interface TabData {
id: number;
type: string;
title: string;
terminalRef?: {
current?: {
fit?: () => void;
notifyResize?: () => void;
refresh?: () => void;
};
};
hostConfig?: unknown;
[key: string]: unknown;
}
interface TerminalViewProps { interface TerminalViewProps {
isTopbarOpen?: boolean; isTopbarOpen?: boolean;
} }
@@ -25,11 +40,16 @@ interface TerminalViewProps {
export function AppView({ export function AppView({
isTopbarOpen = true, isTopbarOpen = true,
}: TerminalViewProps): React.ReactElement { }: TerminalViewProps): React.ReactElement {
const { tabs, currentTab, allSplitScreenTab, removeTab } = useTabs() as any; const { tabs, currentTab, allSplitScreenTab, removeTab } = useTabs() as {
tabs: TabData[];
currentTab: number;
allSplitScreenTab: number[];
removeTab: (id: number) => void;
};
const { state: sidebarState } = useSidebar(); const { state: sidebarState } = useSidebar();
const terminalTabs = tabs.filter( const terminalTabs = tabs.filter(
(tab: any) => (tab: TabData) =>
tab.type === "terminal" || tab.type === "terminal" ||
tab.type === "server" || tab.type === "server" ||
tab.type === "file_manager", tab.type === "file_manager",
@@ -59,7 +79,7 @@ export function AppView({
const splitIds = allSplitScreenTab as number[]; const splitIds = allSplitScreenTab as number[];
visibleIds.push(currentTab, ...splitIds.filter((i) => i !== currentTab)); visibleIds.push(currentTab, ...splitIds.filter((i) => i !== currentTab));
} }
terminalTabs.forEach((t: any) => { terminalTabs.forEach((t: TabData) => {
if (visibleIds.includes(t.id)) { if (visibleIds.includes(t.id)) {
const ref = t.terminalRef?.current; const ref = t.terminalRef?.current;
if (ref?.fit) ref.fit(); if (ref?.fit) ref.fit();
@@ -125,16 +145,16 @@ export function AppView({
const renderTerminalsLayer = () => { const renderTerminalsLayer = () => {
const styles: Record<number, React.CSSProperties> = {}; const styles: Record<number, React.CSSProperties> = {};
const splitTabs = terminalTabs.filter((tab: any) => const splitTabs = terminalTabs.filter((tab: TabData) =>
allSplitScreenTab.includes(tab.id), allSplitScreenTab.includes(tab.id),
); );
const mainTab = terminalTabs.find((tab: any) => tab.id === currentTab); const mainTab = terminalTabs.find((tab: TabData) => tab.id === currentTab);
const layoutTabs = [ const layoutTabs = [
mainTab, mainTab,
...splitTabs.filter( ...splitTabs.filter(
(t: any) => t && t.id !== (mainTab && (mainTab as any).id), (t: TabData) => t && t.id !== (mainTab && (mainTab as TabData).id),
), ),
].filter(Boolean) as any[]; ].filter((t): t is TabData => t !== null && t !== undefined);
if (allSplitScreenTab.length === 0 && mainTab) { if (allSplitScreenTab.length === 0 && mainTab) {
const isFileManagerTab = mainTab.type === "file_manager"; const isFileManagerTab = mainTab.type === "file_manager";
@@ -150,7 +170,7 @@ export function AppView({
opacity: ready ? 1 : 0, opacity: ready ? 1 : 0,
}; };
} else { } else {
layoutTabs.forEach((t: any) => { layoutTabs.forEach((t: TabData) => {
const rect = panelRects[String(t.id)]; const rect = panelRects[String(t.id)];
const parentRect = containerRef.current?.getBoundingClientRect(); const parentRect = containerRef.current?.getBoundingClientRect();
if (rect && parentRect) { if (rect && parentRect) {
@@ -171,7 +191,7 @@ export function AppView({
return ( return (
<div className="absolute inset-0 z-[1]"> <div className="absolute inset-0 z-[1]">
{terminalTabs.map((t: any) => { {terminalTabs.map((t: TabData) => {
const hasStyle = !!styles[t.id]; const hasStyle = !!styles[t.id];
const isVisible = const isVisible =
hasStyle || (allSplitScreenTab.length === 0 && t.id === currentTab); hasStyle || (allSplitScreenTab.length === 0 && t.id === currentTab);
@@ -241,16 +261,16 @@ export function AppView({
}; };
const renderSplitOverlays = () => { const renderSplitOverlays = () => {
const splitTabs = terminalTabs.filter((tab: any) => const splitTabs = terminalTabs.filter((tab: TabData) =>
allSplitScreenTab.includes(tab.id), allSplitScreenTab.includes(tab.id),
); );
const mainTab = terminalTabs.find((tab: any) => tab.id === currentTab); const mainTab = terminalTabs.find((tab: TabData) => tab.id === currentTab);
const layoutTabs = [ const layoutTabs = [
mainTab, mainTab,
...splitTabs.filter( ...splitTabs.filter(
(t: any) => t && t.id !== (mainTab && (mainTab as any).id), (t: TabData) => t && t.id !== (mainTab && (mainTab as TabData).id),
), ),
].filter(Boolean) as any[]; ].filter((t): t is TabData => t !== null && t !== undefined);
if (allSplitScreenTab.length === 0) return null; if (allSplitScreenTab.length === 0) return null;
const handleStyle = { const handleStyle = {
@@ -258,13 +278,16 @@ export function AppView({
zIndex: 12, zIndex: 12,
background: "var(--color-dark-border)", background: "var(--color-dark-border)",
} as React.CSSProperties; } as React.CSSProperties;
const commonGroupProps = { const commonGroupProps: {
onLayout: () => void;
onResize: () => void;
} = {
onLayout: scheduleMeasureAndFit, onLayout: scheduleMeasureAndFit,
onResize: scheduleMeasureAndFit, onResize: scheduleMeasureAndFit,
} as any; };
if (layoutTabs.length === 2) { if (layoutTabs.length === 2) {
const [a, b] = layoutTabs as any[]; const [a, b] = layoutTabs;
return ( return (
<div className="absolute inset-0 z-[10] pointer-events-none"> <div className="absolute inset-0 z-[10] pointer-events-none">
<ResizablePrimitive.PanelGroup <ResizablePrimitive.PanelGroup
@@ -316,7 +339,7 @@ export function AppView({
); );
} }
if (layoutTabs.length === 3) { if (layoutTabs.length === 3) {
const [a, b, c] = layoutTabs as any[]; const [a, b, c] = layoutTabs;
return ( return (
<div className="absolute inset-0 z-[10] pointer-events-none"> <div className="absolute inset-0 z-[10] pointer-events-none">
<ResizablePrimitive.PanelGroup <ResizablePrimitive.PanelGroup
@@ -404,7 +427,7 @@ export function AppView({
); );
} }
if (layoutTabs.length === 4) { if (layoutTabs.length === 4) {
const [a, b, c, d] = layoutTabs as any[]; const [a, b, c, d] = layoutTabs;
return ( return (
<div className="absolute inset-0 z-[10] pointer-events-none"> <div className="absolute inset-0 z-[10] pointer-events-none">
<ResizablePrimitive.PanelGroup <ResizablePrimitive.PanelGroup
@@ -529,7 +552,7 @@ export function AppView({
return null; return null;
}; };
const currentTabData = tabs.find((tab: any) => tab.id === currentTab); const currentTabData = tabs.find((tab: TabData) => tab.id === currentTab);
const isFileManager = currentTabData?.type === "file_manager"; const isFileManager = currentTabData?.type === "file_manager";
const isSplitScreen = allSplitScreenTab.length > 0; const isSplitScreen = allSplitScreenTab.length > 0;

View File

@@ -57,7 +57,7 @@ interface SSHHost {
enableTunnel: boolean; enableTunnel: boolean;
enableFileManager: boolean; enableFileManager: boolean;
defaultPath: string; defaultPath: string;
tunnelConnections: any[]; tunnelConnections: unknown[];
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
} }
@@ -112,13 +112,19 @@ export function LeftSidebar({
setCurrentTab, setCurrentTab,
allSplitScreenTab, allSplitScreenTab,
updateHostConfig, updateHostConfig,
} = useTabs() as any; } = useTabs() as {
tabs: Array<{ id: number; type: string; [key: string]: unknown }>;
addTab: (tab: { type: string; [key: string]: unknown }) => number;
setCurrentTab: (id: number) => void;
allSplitScreenTab: number[];
updateHostConfig: (id: number, config: unknown) => void;
};
const isSplitScreenActive = const isSplitScreenActive =
Array.isArray(allSplitScreenTab) && allSplitScreenTab.length > 0; Array.isArray(allSplitScreenTab) && allSplitScreenTab.length > 0;
const sshManagerTab = tabList.find((t) => t.type === "ssh_manager"); const sshManagerTab = tabList.find((t) => t.type === "ssh_manager");
const openSshManagerTab = () => { const openSshManagerTab = () => {
if (sshManagerTab || isSplitScreenActive) return; if (sshManagerTab || isSplitScreenActive) return;
const id = addTab({ type: "ssh_manager" } as any); const id = addTab({ type: "ssh_manager" });
setCurrentTab(id); setCurrentTab(id);
}; };
const adminTab = tabList.find((t) => t.type === "admin"); const adminTab = tabList.find((t) => t.type === "admin");
@@ -128,7 +134,7 @@ export function LeftSidebar({
setCurrentTab(adminTab.id); setCurrentTab(adminTab.id);
return; return;
} }
const id = addTab({ type: "admin" } as any); const id = addTab({ type: "admin" });
setCurrentTab(id); setCurrentTab(id);
}; };
const userProfileTab = tabList.find((t) => t.type === "user_profile"); const userProfileTab = tabList.find((t) => t.type === "user_profile");
@@ -138,7 +144,7 @@ export function LeftSidebar({
setCurrentTab(userProfileTab.id); setCurrentTab(userProfileTab.id);
return; return;
} }
const id = addTab({ type: "user_profile" } as any); const id = addTab({ type: "user_profile" });
setCurrentTab(id); setCurrentTab(id);
}; };
@@ -206,7 +212,7 @@ export function LeftSidebar({
}); });
}, 50); }, 50);
} }
} catch (err: any) { } catch (err: unknown) {
setHostsError(t("leftSidebar.failedToLoadHosts")); setHostsError(t("leftSidebar.failedToLoadHosts"));
} }
}, [updateHostConfig]); }, [updateHostConfig]);
@@ -319,9 +325,10 @@ export function LeftSidebar({
await deleteAccount(deletePassword); await deleteAccount(deletePassword);
handleLogout(); handleLogout();
} catch (err: any) { } catch (err: unknown) {
setDeleteError( setDeleteError(
err?.response?.data?.error || t("leftSidebar.failedToDeleteAccount"), (err as { response?: { data?: { error?: string } } })?.response?.data
?.error || t("leftSidebar.failedToDeleteAccount"),
); );
setDeleteLoading(false); setDeleteLoading(false);
} }

View File

@@ -18,6 +18,18 @@ import { TabDropdown } from "@/ui/Desktop/Navigation/Tabs/TabDropdown.tsx";
import { getCookie, setCookie } from "@/ui/main-axios.ts"; import { getCookie, setCookie } from "@/ui/main-axios.ts";
import { SnippetsSidebar } from "@/ui/Desktop/Apps/Terminal/SnippetsSidebar.tsx"; import { SnippetsSidebar } from "@/ui/Desktop/Apps/Terminal/SnippetsSidebar.tsx";
interface TabData {
id: number;
type: string;
title: string;
terminalRef?: {
current?: {
sendInput?: (data: string) => void;
};
};
[key: string]: unknown;
}
interface TopNavbarProps { interface TopNavbarProps {
isTopbarOpen: boolean; isTopbarOpen: boolean;
setIsTopbarOpen: (open: boolean) => void; setIsTopbarOpen: (open: boolean) => void;
@@ -35,7 +47,14 @@ export function TopNavbar({
setSplitScreenTab, setSplitScreenTab,
removeTab, removeTab,
allSplitScreenTab, allSplitScreenTab,
} = useTabs() as any; } = useTabs() as {
tabs: TabData[];
currentTab: number;
setCurrentTab: (id: number) => void;
setSplitScreenTab: (id: number) => void;
removeTab: (id: number) => void;
allSplitScreenTab: number[];
};
const leftPosition = state === "collapsed" ? "26px" : "264px"; const leftPosition = state === "collapsed" ? "26px" : "264px";
const { t } = useTranslation(); const { t } = useTranslation();
@@ -192,7 +211,7 @@ export function TopNavbar({
if (commandToSend) { if (commandToSend) {
selectedTabIds.forEach((tabId) => { selectedTabIds.forEach((tabId) => {
const tab = tabs.find((t: any) => t.id === tabId); const tab = tabs.find((t: TabData) => t.id === tabId);
if (tab?.terminalRef?.current?.sendInput) { if (tab?.terminalRef?.current?.sendInput) {
tab.terminalRef.current.sendInput(commandToSend); tab.terminalRef.current.sendInput(commandToSend);
} }
@@ -206,7 +225,7 @@ export function TopNavbar({
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) { if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
const char = e.key; const char = e.key;
selectedTabIds.forEach((tabId) => { selectedTabIds.forEach((tabId) => {
const tab = tabs.find((t: any) => t.id === tabId); const tab = tabs.find((t: TabData) => t.id === tabId);
if (tab?.terminalRef?.current?.sendInput) { if (tab?.terminalRef?.current?.sendInput) {
tab.terminalRef.current.sendInput(char); tab.terminalRef.current.sendInput(char);
} }
@@ -215,7 +234,7 @@ export function TopNavbar({
}; };
const handleSnippetExecute = (content: string) => { const handleSnippetExecute = (content: string) => {
const tab = tabs.find((t: any) => t.id === currentTab); const tab = tabs.find((t: TabData) => t.id === currentTab);
if (tab?.terminalRef?.current?.sendInput) { if (tab?.terminalRef?.current?.sendInput) {
tab.terminalRef.current.sendInput(content + "\n"); tab.terminalRef.current.sendInput(content + "\n");
} }
@@ -223,13 +242,13 @@ export function TopNavbar({
const isSplitScreenActive = const isSplitScreenActive =
Array.isArray(allSplitScreenTab) && allSplitScreenTab.length > 0; Array.isArray(allSplitScreenTab) && allSplitScreenTab.length > 0;
const currentTabObj = tabs.find((t: any) => t.id === currentTab); const currentTabObj = tabs.find((t: TabData) => t.id === currentTab);
const currentTabIsHome = currentTabObj?.type === "home"; const currentTabIsHome = currentTabObj?.type === "home";
const currentTabIsSshManager = currentTabObj?.type === "ssh_manager"; const currentTabIsSshManager = currentTabObj?.type === "ssh_manager";
const currentTabIsAdmin = currentTabObj?.type === "admin"; const currentTabIsAdmin = currentTabObj?.type === "admin";
const currentTabIsUserProfile = currentTabObj?.type === "user_profile"; const currentTabIsUserProfile = currentTabObj?.type === "user_profile";
const terminalTabs = tabs.filter((tab: any) => tab.type === "terminal"); const terminalTabs = tabs.filter((tab: TabData) => tab.type === "terminal");
const updateRightClickCopyPaste = (checked: boolean) => { const updateRightClickCopyPaste = (checked: boolean) => {
setCookie("rightClickCopyPaste", checked.toString()); setCookie("rightClickCopyPaste", checked.toString());
@@ -246,7 +265,7 @@ export function TopNavbar({
}} }}
> >
<div className="h-full p-1 pr-2 border-r-2 border-dark-border w-[calc(100%-6rem)] flex items-center overflow-x-auto overflow-y-hidden gap-2 thin-scrollbar"> <div className="h-full p-1 pr-2 border-r-2 border-dark-border w-[calc(100%-6rem)] flex items-center overflow-x-auto overflow-y-hidden gap-2 thin-scrollbar">
{tabs.map((tab: any) => { {tabs.map((tab: TabData) => {
const isActive = tab.id === currentTab; const isActive = tab.id === currentTab;
const isSplit = const isSplit =
Array.isArray(allSplitScreenTab) && Array.isArray(allSplitScreenTab) &&

View File

@@ -14,379 +14,412 @@ import { useTranslation } from "react-i18next";
import { isElectron, getCookie } from "@/ui/main-axios.ts"; import { isElectron, getCookie } from "@/ui/main-axios.ts";
import { toast } from "sonner"; import { toast } from "sonner";
interface HostConfig {
id?: number;
ip: string;
port: number;
username: string;
password?: string;
key?: string;
keyPassword?: string;
keyType?: string;
authType?: string;
credentialId?: number;
[key: string]: unknown;
}
interface TerminalHandle {
disconnect: () => void;
fit: () => void;
sendInput: (data: string) => void;
notifyResize: () => void;
refresh: () => void;
}
interface SSHTerminalProps { interface SSHTerminalProps {
hostConfig: any; hostConfig: HostConfig;
isVisible: boolean; isVisible: boolean;
title?: string; title?: string;
} }
export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal( export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
{ hostConfig, isVisible }, function SSHTerminal({ hostConfig, isVisible }, ref) {
ref, const { t } = useTranslation();
) { const { instance: terminal, ref: xtermRef } = useXTerm();
const { t } = useTranslation(); const fitAddonRef = useRef<FitAddon | null>(null);
const { instance: terminal, ref: xtermRef } = useXTerm(); const webSocketRef = useRef<WebSocket | null>(null);
const fitAddonRef = useRef<FitAddon | null>(null); const resizeTimeout = useRef<NodeJS.Timeout | null>(null);
const webSocketRef = useRef<WebSocket | null>(null); const wasDisconnectedBySSH = useRef(false);
const resizeTimeout = useRef<NodeJS.Timeout | null>(null); const pingIntervalRef = useRef<NodeJS.Timeout | null>(null);
const wasDisconnectedBySSH = useRef(false); const [visible, setVisible] = useState(false);
const pingIntervalRef = useRef<NodeJS.Timeout | null>(null); const [isConnected, setIsConnected] = useState(false);
const [visible, setVisible] = useState(false); const [isConnecting, setIsConnecting] = useState(false);
const [isConnected, setIsConnected] = useState(false); const [connectionError, setConnectionError] = useState<string | null>(null);
const [isConnecting, setIsConnecting] = useState(false); const [isAuthenticated, setIsAuthenticated] = useState(false);
const [connectionError, setConnectionError] = useState<string | null>(null); const isVisibleRef = useRef<boolean>(false);
const [isAuthenticated, setIsAuthenticated] = useState(false); const isConnectingRef = useRef(false);
const isVisibleRef = useRef<boolean>(false);
const isConnectingRef = useRef(false);
const lastSentSizeRef = useRef<{ cols: number; rows: number } | null>(null); const lastSentSizeRef = useRef<{ cols: number; rows: number } | null>(null);
const pendingSizeRef = useRef<{ cols: number; rows: number } | null>(null); const pendingSizeRef = useRef<{ cols: number; rows: number } | null>(null);
const notifyTimerRef = useRef<NodeJS.Timeout | null>(null); const notifyTimerRef = useRef<NodeJS.Timeout | null>(null);
const DEBOUNCE_MS = 140; const DEBOUNCE_MS = 140;
useEffect(() => { useEffect(() => {
isVisibleRef.current = isVisible; isVisibleRef.current = isVisible;
}, [isVisible]); }, [isVisible]);
useEffect(() => { useEffect(() => {
const checkAuth = () => { const checkAuth = () => {
const jwtToken = getCookie("jwt"); const jwtToken = getCookie("jwt");
const isAuth = !!(jwtToken && jwtToken.trim() !== ""); const isAuth = !!(jwtToken && jwtToken.trim() !== "");
setIsAuthenticated((prev) => { setIsAuthenticated((prev) => {
if (prev !== isAuth) { if (prev !== isAuth) {
return isAuth; return isAuth;
}
return prev;
});
};
checkAuth();
const authCheckInterval = setInterval(checkAuth, 5000);
return () => clearInterval(authCheckInterval);
}, []);
function hardRefresh() {
try {
if (
terminal &&
typeof (
terminal as { refresh?: (start: number, end: number) => void }
).refresh === "function"
) {
(
terminal as { refresh?: (start: number, end: number) => void }
).refresh(0, terminal.rows - 1);
} }
return prev; } catch {
}); // Ignore terminal refresh errors
};
checkAuth();
const authCheckInterval = setInterval(checkAuth, 5000);
return () => clearInterval(authCheckInterval);
}, []);
function hardRefresh() {
try {
if (terminal && typeof (terminal as any).refresh === "function") {
(terminal as any).refresh(0, terminal.rows - 1);
} }
} catch {
// Ignore terminal refresh errors
} }
}
function scheduleNotify(cols: number, rows: number) { function scheduleNotify(cols: number, rows: number) {
if (!(cols > 0 && rows > 0)) return; if (!(cols > 0 && rows > 0)) return;
pendingSizeRef.current = { cols, rows }; pendingSizeRef.current = { cols, rows };
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current); if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
notifyTimerRef.current = setTimeout(() => { notifyTimerRef.current = setTimeout(() => {
const next = pendingSizeRef.current; const next = pendingSizeRef.current;
const last = lastSentSizeRef.current; const last = lastSentSizeRef.current;
if (!next) return; if (!next) return;
if (last && last.cols === next.cols && last.rows === next.rows) return; if (last && last.cols === next.cols && last.rows === next.rows) return;
if (webSocketRef.current?.readyState === WebSocket.OPEN) { if (webSocketRef.current?.readyState === WebSocket.OPEN) {
webSocketRef.current.send( webSocketRef.current.send(
JSON.stringify({ type: "resize", data: next }), JSON.stringify({ type: "resize", data: next }),
);
lastSentSizeRef.current = next;
}
}, DEBOUNCE_MS);
}
useImperativeHandle(
ref,
() => ({
disconnect: () => {
if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current);
pingIntervalRef.current = null;
}
webSocketRef.current?.close();
},
fit: () => {
fitAddonRef.current?.fit();
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
hardRefresh();
},
sendInput: (data: string) => {
if (webSocketRef.current?.readyState === 1) {
webSocketRef.current.send(JSON.stringify({ type: "input", data }));
}
},
notifyResize: () => {
try {
const cols = terminal?.cols ?? undefined;
const rows = terminal?.rows ?? undefined;
if (typeof cols === "number" && typeof rows === "number") {
scheduleNotify(cols, rows);
hardRefresh();
}
} catch {
// Ignore resize notification errors
}
},
refresh: () => hardRefresh(),
}),
[terminal],
);
function handleWindowResize() {
if (!isVisibleRef.current) return;
fitAddonRef.current?.fit();
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
hardRefresh();
}
function setupWebSocketListeners(
ws: WebSocket,
cols: number,
rows: number,
) {
ws.addEventListener("open", () => {
ws.send(
JSON.stringify({
type: "connectToHost",
data: { cols, rows, hostConfig },
}),
); );
lastSentSizeRef.current = next; terminal.onData((data) => {
} ws.send(JSON.stringify({ type: "input", data }));
}, DEBOUNCE_MS); });
}
useImperativeHandle( pingIntervalRef.current = setInterval(() => {
ref, if (ws.readyState === WebSocket.OPEN) {
() => ({ ws.send(JSON.stringify({ type: "ping" }));
disconnect: () => { }
}, 30000);
});
ws.addEventListener("message", (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.type === "data") {
if (typeof msg.data === "string") {
terminal.write(msg.data);
} else {
terminal.write(String(msg.data));
}
} else if (msg.type === "error")
terminal.writeln(`\r\n[${t("terminal.error")}] ${msg.message}`);
else if (msg.type === "connected") {
isConnectingRef.current = false;
} else if (msg.type === "disconnected") {
wasDisconnectedBySSH.current = true;
isConnectingRef.current = false;
terminal.writeln(
`\r\n[${msg.message || t("terminal.disconnected")}]`,
);
}
} catch {
// Ignore message parsing errors
}
});
ws.addEventListener("close", (event) => {
isConnectingRef.current = false;
if (event.code === 1008) {
console.error("WebSocket authentication failed:", event.reason);
terminal.writeln(`\r\n[Authentication failed - please re-login]`);
localStorage.removeItem("jwt");
return;
}
if (!wasDisconnectedBySSH.current) {
terminal.writeln(`\r\n[${t("terminal.connectionClosed")}]`);
}
});
ws.addEventListener("error", () => {
isConnectingRef.current = false;
terminal.writeln(`\r\n[${t("terminal.connectionError")}]`);
});
}
useEffect(() => {
if (!terminal || !xtermRef.current || !hostConfig) return;
if (!isAuthenticated) {
return;
}
terminal.options = {
cursorBlink: false,
cursorStyle: "bar",
scrollback: 10000,
fontSize: 14,
fontFamily:
'"Caskaydia Cove Nerd Font Mono", "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
theme: { background: "#09090b", foreground: "#f7f7f7" },
allowTransparency: true,
convertEol: true,
windowsMode: false,
macOptionIsMeta: false,
macOptionClickForcesSelection: false,
rightClickSelectsWord: false,
fastScrollModifier: "alt",
fastScrollSensitivity: 5,
allowProposedApi: true,
disableStdin: true,
cursorInactiveStyle: "bar",
minimumContrastRatio: 1,
letterSpacing: 0,
lineHeight: 1.2,
};
const fitAddon = new FitAddon();
const clipboardAddon = new ClipboardAddon();
const unicode11Addon = new Unicode11Addon();
const webLinksAddon = new WebLinksAddon();
fitAddonRef.current = fitAddon;
terminal.loadAddon(fitAddon);
terminal.loadAddon(clipboardAddon);
terminal.loadAddon(unicode11Addon);
terminal.loadAddon(webLinksAddon);
terminal.unicode.activeVersion = "11";
terminal.open(xtermRef.current);
const textarea = xtermRef.current.querySelector(
".xterm-helper-textarea",
) as HTMLTextAreaElement | null;
if (textarea) {
textarea.readOnly = true;
textarea.blur();
}
terminal.focus = () => {};
const resizeObserver = new ResizeObserver(() => {
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
resizeTimeout.current = setTimeout(() => {
if (!isVisibleRef.current) return;
fitAddonRef.current?.fit();
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
hardRefresh();
}, 150);
});
resizeObserver.observe(xtermRef.current);
const readyFonts =
(document as { fonts?: { ready?: Promise<unknown> } }).fonts
?.ready instanceof Promise
? (document as { fonts?: { ready?: Promise<unknown> } }).fonts.ready
: Promise.resolve();
setVisible(true);
readyFonts.then(() => {
setTimeout(() => {
fitAddon.fit();
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
hardRefresh();
const jwtToken = getCookie("jwt");
if (!jwtToken || jwtToken.trim() === "") {
setIsConnected(false);
setIsConnecting(false);
setConnectionError("Authentication required");
return;
}
const cols = terminal.cols;
const rows = terminal.rows;
const isDev =
process.env.NODE_ENV === "development" &&
(window.location.port === "3000" ||
window.location.port === "5173" ||
window.location.port === "");
const baseWsUrl = isDev
? `${window.location.protocol === "https:" ? "wss" : "ws"}://localhost:30002`
: isElectron()
? (() => {
const baseUrl =
(window as { configuredServerUrl?: string })
.configuredServerUrl || "http://127.0.0.1:30001";
const wsProtocol = baseUrl.startsWith("https://")
? "wss://"
: "ws://";
const wsHost = baseUrl.replace(/^https?:\/\//, "");
return `${wsProtocol}${wsHost}/ssh/websocket/`;
})()
: `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/ssh/websocket/`;
if (isConnectingRef.current) {
return;
}
isConnectingRef.current = true;
if (
webSocketRef.current &&
webSocketRef.current.readyState !== WebSocket.CLOSED
) {
webSocketRef.current.close();
}
if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current);
pingIntervalRef.current = null;
}
const wsUrl = `${baseWsUrl}?token=${encodeURIComponent(jwtToken)}`;
setIsConnecting(true);
setConnectionError(null);
const ws = new WebSocket(wsUrl);
webSocketRef.current = ws;
wasDisconnectedBySSH.current = false;
setupWebSocketListeners(ws, cols, rows);
}, 200);
});
return () => {
resizeObserver.disconnect();
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
if (pingIntervalRef.current) { if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current); clearInterval(pingIntervalRef.current);
pingIntervalRef.current = null; pingIntervalRef.current = null;
} }
webSocketRef.current?.close(); webSocketRef.current?.close();
}, };
fit: () => { }, [xtermRef, terminal, hostConfig]);
fitAddonRef.current?.fit();
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
hardRefresh();
},
sendInput: (data: string) => {
if (webSocketRef.current?.readyState === 1) {
webSocketRef.current.send(JSON.stringify({ type: "input", data }));
}
},
notifyResize: () => {
try {
const cols = terminal?.cols ?? undefined;
const rows = terminal?.rows ?? undefined;
if (typeof cols === "number" && typeof rows === "number") {
scheduleNotify(cols, rows);
hardRefresh();
}
} catch {
// Ignore resize notification errors
}
},
refresh: () => hardRefresh(),
}),
[terminal],
);
function handleWindowResize() { useEffect(() => {
if (!isVisibleRef.current) return; if (isVisible && fitAddonRef.current) {
fitAddonRef.current?.fit(); setTimeout(() => {
if (terminal) scheduleNotify(terminal.cols, terminal.rows); fitAddonRef.current?.fit();
hardRefresh(); if (terminal) scheduleNotify(terminal.cols, terminal.rows);
} hardRefresh();
}, 0);
function setupWebSocketListeners(ws: WebSocket, cols: number, rows: number) {
ws.addEventListener("open", () => {
ws.send(
JSON.stringify({
type: "connectToHost",
data: { cols, rows, hostConfig },
}),
);
terminal.onData((data) => {
ws.send(JSON.stringify({ type: "input", data }));
});
pingIntervalRef.current = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "ping" }));
}
}, 30000);
});
ws.addEventListener("message", (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.type === "data") {
if (typeof msg.data === "string") {
terminal.write(msg.data);
} else {
terminal.write(String(msg.data));
}
} else if (msg.type === "error")
terminal.writeln(`\r\n[${t("terminal.error")}] ${msg.message}`);
else if (msg.type === "connected") {
isConnectingRef.current = false;
} else if (msg.type === "disconnected") {
wasDisconnectedBySSH.current = true;
isConnectingRef.current = false;
terminal.writeln(
`\r\n[${msg.message || t("terminal.disconnected")}]`,
);
}
} catch {
// Ignore message parsing errors
} }
}); }, [isVisible, terminal]);
ws.addEventListener("close", (event) => { useEffect(() => {
isConnectingRef.current = false; if (!fitAddonRef.current) return;
if (event.code === 1008) {
console.error("WebSocket authentication failed:", event.reason);
terminal.writeln(`\r\n[Authentication failed - please re-login]`);
localStorage.removeItem("jwt");
return;
}
if (!wasDisconnectedBySSH.current) {
terminal.writeln(`\r\n[${t("terminal.connectionClosed")}]`);
}
});
ws.addEventListener("error", () => {
isConnectingRef.current = false;
terminal.writeln(`\r\n[${t("terminal.connectionError")}]`);
});
}
useEffect(() => {
if (!terminal || !xtermRef.current || !hostConfig) return;
if (!isAuthenticated) {
return;
}
terminal.options = {
cursorBlink: false,
cursorStyle: "bar",
scrollback: 10000,
fontSize: 14,
fontFamily:
'"Caskaydia Cove Nerd Font Mono", "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
theme: { background: "#09090b", foreground: "#f7f7f7" },
allowTransparency: true,
convertEol: true,
windowsMode: false,
macOptionIsMeta: false,
macOptionClickForcesSelection: false,
rightClickSelectsWord: false,
fastScrollModifier: "alt",
fastScrollSensitivity: 5,
allowProposedApi: true,
disableStdin: true,
cursorInactiveStyle: "bar",
minimumContrastRatio: 1,
letterSpacing: 0,
lineHeight: 1.2,
};
const fitAddon = new FitAddon();
const clipboardAddon = new ClipboardAddon();
const unicode11Addon = new Unicode11Addon();
const webLinksAddon = new WebLinksAddon();
fitAddonRef.current = fitAddon;
terminal.loadAddon(fitAddon);
terminal.loadAddon(clipboardAddon);
terminal.loadAddon(unicode11Addon);
terminal.loadAddon(webLinksAddon);
terminal.unicode.activeVersion = "11";
terminal.open(xtermRef.current);
const textarea = xtermRef.current.querySelector(
".xterm-helper-textarea",
) as HTMLTextAreaElement | null;
if (textarea) {
textarea.readOnly = true;
textarea.blur();
}
terminal.focus = () => {};
const resizeObserver = new ResizeObserver(() => {
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
resizeTimeout.current = setTimeout(() => {
if (!isVisibleRef.current) return;
fitAddonRef.current?.fit();
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
hardRefresh();
}, 150);
});
resizeObserver.observe(xtermRef.current);
const readyFonts =
(document as any).fonts?.ready instanceof Promise
? (document as any).fonts.ready
: Promise.resolve();
setVisible(true);
readyFonts.then(() => {
setTimeout(() => {
fitAddon.fit();
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
hardRefresh();
const jwtToken = getCookie("jwt");
if (!jwtToken || jwtToken.trim() === "") {
setIsConnected(false);
setIsConnecting(false);
setConnectionError("Authentication required");
return;
}
const cols = terminal.cols;
const rows = terminal.rows;
const isDev =
process.env.NODE_ENV === "development" &&
(window.location.port === "3000" ||
window.location.port === "5173" ||
window.location.port === "");
const baseWsUrl = isDev
? `${window.location.protocol === "https:" ? "wss" : "ws"}://localhost:30002`
: isElectron()
? (() => {
const baseUrl =
(window as any).configuredServerUrl ||
"http://127.0.0.1:30001";
const wsProtocol = baseUrl.startsWith("https://")
? "wss://"
: "ws://";
const wsHost = baseUrl.replace(/^https?:\/\//, "");
return `${wsProtocol}${wsHost}/ssh/websocket/`;
})()
: `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/ssh/websocket/`;
if (isConnectingRef.current) {
return;
}
isConnectingRef.current = true;
if (
webSocketRef.current &&
webSocketRef.current.readyState !== WebSocket.CLOSED
) {
webSocketRef.current.close();
}
if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current);
pingIntervalRef.current = null;
}
const wsUrl = `${baseWsUrl}?token=${encodeURIComponent(jwtToken)}`;
setIsConnecting(true);
setConnectionError(null);
const ws = new WebSocket(wsUrl);
webSocketRef.current = ws;
wasDisconnectedBySSH.current = false;
setupWebSocketListeners(ws, cols, rows);
}, 200);
});
return () => {
resizeObserver.disconnect();
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current);
pingIntervalRef.current = null;
}
webSocketRef.current?.close();
};
}, [xtermRef, terminal, hostConfig]);
useEffect(() => {
if (isVisible && fitAddonRef.current) {
setTimeout(() => { setTimeout(() => {
fitAddonRef.current?.fit(); fitAddonRef.current?.fit();
if (terminal) scheduleNotify(terminal.cols, terminal.rows); if (terminal) scheduleNotify(terminal.cols, terminal.rows);
hardRefresh(); hardRefresh();
}, 0); }, 0);
} }, [isVisible, terminal]);
}, [isVisible, terminal]);
useEffect(() => { return (
if (!fitAddonRef.current) return; <div
setTimeout(() => { ref={xtermRef}
fitAddonRef.current?.fit(); className={`h-full w-full m-1 transition-opacity duration-200 ${visible && isVisible ? "opacity-100" : "opacity-0"} overflow-hidden`}
if (terminal) scheduleNotify(terminal.cols, terminal.rows); />
hardRefresh(); );
}, 0); },
}, [isVisible, terminal]); );
return (
<div
ref={xtermRef}
className={`h-full w-full m-1 transition-opacity duration-200 ${visible && isVisible ? "opacity-100" : "opacity-0"} overflow-hidden`}
/>
);
});
const style = document.createElement("style"); const style = document.createElement("style");
style.innerHTML = ` style.innerHTML = `

View File

@@ -95,8 +95,22 @@ interface OIDCAuthorize {
export function isElectron(): boolean { export function isElectron(): boolean {
return ( return (
(window as any).IS_ELECTRON === true || (
(window as any).electronAPI?.isElectron === true window as Window &
typeof globalThis & {
IS_ELECTRON?: boolean;
electronAPI?: unknown;
configuredServerUrl?: string;
}
).IS_ELECTRON === true ||
(
window as Window &
typeof globalThis & {
IS_ELECTRON?: boolean;
electronAPI?: unknown;
configuredServerUrl?: string;
}
).electronAPI?.isElectron === true
); );
} }
@@ -154,8 +168,8 @@ function createApiInstance(
const startTime = performance.now(); const startTime = performance.now();
const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
(config as any).startTime = startTime; (config as Record<string, unknown>).startTime = startTime;
(config as any).requestId = requestId; (config as Record<string, unknown>).requestId = requestId;
const method = config.method?.toUpperCase() || "UNKNOWN"; const method = config.method?.toUpperCase() || "UNKNOWN";
const url = config.url || "UNKNOWN"; const url = config.url || "UNKNOWN";
@@ -189,8 +203,8 @@ function createApiInstance(
instance.interceptors.response.use( instance.interceptors.response.use(
(response) => { (response) => {
const endTime = performance.now(); const endTime = performance.now();
const startTime = (response.config as any).startTime; const startTime = (response.config as Record<string, unknown>).startTime;
const requestId = (response.config as any).requestId; const requestId = (response.config as Record<string, unknown>).requestId;
const responseTime = Math.round(endTime - startTime); const responseTime = Math.round(endTime - startTime);
const method = response.config.method?.toUpperCase() || "UNKNOWN"; const method = response.config.method?.toUpperCase() || "UNKNOWN";
@@ -227,8 +241,10 @@ function createApiInstance(
}, },
(error: AxiosError) => { (error: AxiosError) => {
const endTime = performance.now(); const endTime = performance.now();
const startTime = (error.config as any)?.startTime; const startTime = (error.config as Record<string, unknown> | undefined)
const requestId = (error.config as any)?.requestId; ?.startTime;
const requestId = (error.config as Record<string, unknown> | undefined)
?.requestId;
const responseTime = startTime const responseTime = startTime
? Math.round(endTime - startTime) ? Math.round(endTime - startTime)
: undefined; : undefined;
@@ -238,10 +254,11 @@ function createApiInstance(
const fullUrl = error.config ? `${error.config.baseURL}${url}` : url; const fullUrl = error.config ? `${error.config.baseURL}${url}` : url;
const status = error.response?.status; const status = error.response?.status;
const message = const message =
(error.response?.data as any)?.error || (error.response?.data as Record<string, unknown>)?.error ||
(error as Error).message || (error as Error).message ||
"Unknown error"; "Unknown error";
const errorCode = (error.response?.data as any)?.code || error.code; const errorCode =
(error.response?.data as Record<string, unknown>)?.code || error.code;
const context: LogContext = { const context: LogContext = {
requestId, requestId,
@@ -274,7 +291,8 @@ function createApiInstance(
} }
if (status === 401) { if (status === 401) {
const errorCode = (error.response?.data as any)?.code; const errorCode = (error.response?.data as Record<string, unknown>)
?.code;
const isSessionExpired = errorCode === "SESSION_EXPIRED"; const isSessionExpired = errorCode === "SESSION_EXPIRED";
if (isElectron()) { if (isElectron()) {
@@ -337,9 +355,14 @@ export async function getServerConfig(): Promise<ServerConfig | null> {
if (!isElectron()) return null; if (!isElectron()) return null;
try { try {
const result = await (window as any).electronAPI?.invoke( const result = await (
"get-server-config", window as Window &
); typeof globalThis & {
IS_ELECTRON?: boolean;
electronAPI?: unknown;
configuredServerUrl?: string;
}
).electronAPI?.invoke("get-server-config");
return result; return result;
} catch (error) { } catch (error) {
console.error("Failed to get server config:", error); console.error("Failed to get server config:", error);
@@ -351,13 +374,24 @@ export async function saveServerConfig(config: ServerConfig): Promise<boolean> {
if (!isElectron()) return false; if (!isElectron()) return false;
try { try {
const result = await (window as any).electronAPI?.invoke( const result = await (
"save-server-config", window as Window &
config, typeof globalThis & {
); IS_ELECTRON?: boolean;
electronAPI?: unknown;
configuredServerUrl?: string;
}
).electronAPI?.invoke("save-server-config", config);
if (result?.success) { if (result?.success) {
configuredServerUrl = config.serverUrl; configuredServerUrl = config.serverUrl;
(window as any).configuredServerUrl = configuredServerUrl; (
window as Window &
typeof globalThis & {
IS_ELECTRON?: boolean;
electronAPI?: unknown;
configuredServerUrl?: string;
}
).configuredServerUrl = configuredServerUrl;
updateApiInstances(); updateApiInstances();
return true; return true;
} }
@@ -375,10 +409,14 @@ export async function testServerConnection(
return { success: false, error: "Not in Electron environment" }; return { success: false, error: "Not in Electron environment" };
try { try {
const result = await (window as any).electronAPI?.invoke( const result = await (
"test-server-connection", window as Window &
serverUrl, typeof globalThis & {
); IS_ELECTRON?: boolean;
electronAPI?: unknown;
configuredServerUrl?: string;
}
).electronAPI?.invoke("test-server-connection", serverUrl);
return result; return result;
} catch (error) { } catch (error) {
console.error("Failed to test server connection:", error); console.error("Failed to test server connection:", error);
@@ -406,9 +444,14 @@ export async function checkElectronUpdate(): Promise<{
return { success: false, error: "Not in Electron environment" }; return { success: false, error: "Not in Electron environment" };
try { try {
const result = await (window as any).electronAPI?.invoke( const result = await (
"check-electron-update", window as Window &
); typeof globalThis & {
IS_ELECTRON?: boolean;
electronAPI?: unknown;
configuredServerUrl?: string;
}
).electronAPI?.invoke("check-electron-update");
return result; return result;
} catch (error) { } catch (error) {
console.error("Failed to check Electron update:", error); console.error("Failed to check Electron update:", error);
@@ -472,7 +515,14 @@ if (isElectron()) {
.then((config) => { .then((config) => {
if (config?.serverUrl) { if (config?.serverUrl) {
configuredServerUrl = config.serverUrl; configuredServerUrl = config.serverUrl;
(window as any).configuredServerUrl = configuredServerUrl; (
window as Window &
typeof globalThis & {
IS_ELECTRON?: boolean;
electronAPI?: unknown;
configuredServerUrl?: string;
}
).configuredServerUrl = configuredServerUrl;
} }
initializeApiInstances(); initializeApiInstances();
}) })
@@ -495,7 +545,14 @@ function updateApiInstances() {
initializeApiInstances(); initializeApiInstances();
(window as any).configuredServerUrl = configuredServerUrl; (
window as Window &
typeof globalThis & {
IS_ELECTRON?: boolean;
electronAPI?: unknown;
configuredServerUrl?: string;
}
).configuredServerUrl = configuredServerUrl;
systemLogger.success("All API instances updated successfully", { systemLogger.success("All API instances updated successfully", {
operation: "api_instance_update_complete", operation: "api_instance_update_complete",
@@ -564,7 +621,7 @@ function handleApiError(error: unknown, operation: string): never {
403, 403,
code || "ACCESS_DENIED", code || "ACCESS_DENIED",
); );
(apiError as any).response = error.response; (apiError as ApiError & { response?: unknown }).response = error.response;
throw apiError; throw apiError;
} else if (status === 404) { } else if (status === 404) {
apiLogger.warn(`Not found: ${method} ${url}`, errorContext); apiLogger.warn(`Not found: ${method} ${url}`, errorContext);
@@ -788,7 +845,9 @@ export async function bulkImportSSHHosts(hosts: SSHHostData[]): Promise<{
} }
} }
export async function deleteSSHHost(hostId: number): Promise<any> { export async function deleteSSHHost(
hostId: number,
): Promise<Record<string, unknown>> {
try { try {
const response = await sshHostApi.delete(`/db/host/${hostId}`); const response = await sshHostApi.delete(`/db/host/${hostId}`);
return response.data; return response.data;
@@ -821,7 +880,9 @@ export async function exportSSHHostWithCredentials(
// SSH AUTOSTART MANAGEMENT // SSH AUTOSTART MANAGEMENT
// ============================================================================ // ============================================================================
export async function enableAutoStart(sshConfigId: number): Promise<any> { export async function enableAutoStart(
sshConfigId: number,
): Promise<Record<string, unknown>> {
try { try {
const response = await sshHostApi.post("/autostart/enable", { const response = await sshHostApi.post("/autostart/enable", {
sshConfigId, sshConfigId,
@@ -832,7 +893,9 @@ export async function enableAutoStart(sshConfigId: number): Promise<any> {
} }
} }
export async function disableAutoStart(sshConfigId: number): Promise<any> { export async function disableAutoStart(
sshConfigId: number,
): Promise<Record<string, unknown>> {
try { try {
const response = await sshHostApi.delete("/autostart/disable", { const response = await sshHostApi.delete("/autostart/disable", {
data: { sshConfigId }, data: { sshConfigId },
@@ -883,7 +946,9 @@ export async function getTunnelStatusByName(
return statuses[tunnelName]; return statuses[tunnelName];
} }
export async function connectTunnel(tunnelConfig: TunnelConfig): Promise<any> { export async function connectTunnel(
tunnelConfig: TunnelConfig,
): Promise<Record<string, unknown>> {
try { try {
const response = await tunnelApi.post("/tunnel/connect", tunnelConfig); const response = await tunnelApi.post("/tunnel/connect", tunnelConfig);
return response.data; return response.data;
@@ -892,7 +957,9 @@ export async function connectTunnel(tunnelConfig: TunnelConfig): Promise<any> {
} }
} }
export async function disconnectTunnel(tunnelName: string): Promise<any> { export async function disconnectTunnel(
tunnelName: string,
): Promise<Record<string, unknown>> {
try { try {
const response = await tunnelApi.post("/tunnel/disconnect", { tunnelName }); const response = await tunnelApi.post("/tunnel/disconnect", { tunnelName });
return response.data; return response.data;
@@ -901,7 +968,9 @@ export async function disconnectTunnel(tunnelName: string): Promise<any> {
} }
} }
export async function cancelTunnel(tunnelName: string): Promise<any> { export async function cancelTunnel(
tunnelName: string,
): Promise<Record<string, unknown>> {
try { try {
const response = await tunnelApi.post("/tunnel/cancel", { tunnelName }); const response = await tunnelApi.post("/tunnel/cancel", { tunnelName });
return response.data; return response.data;
@@ -929,7 +998,7 @@ export async function getFileManagerRecent(
export async function addFileManagerRecent( export async function addFileManagerRecent(
file: FileManagerOperation, file: FileManagerOperation,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await sshHostApi.post("/file_manager/recent", file); const response = await sshHostApi.post("/file_manager/recent", file);
return response.data; return response.data;
@@ -940,7 +1009,7 @@ export async function addFileManagerRecent(
export async function removeFileManagerRecent( export async function removeFileManagerRecent(
file: FileManagerOperation, file: FileManagerOperation,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await sshHostApi.delete("/file_manager/recent", { const response = await sshHostApi.delete("/file_manager/recent", {
data: file, data: file,
@@ -966,7 +1035,7 @@ export async function getFileManagerPinned(
export async function addFileManagerPinned( export async function addFileManagerPinned(
file: FileManagerOperation, file: FileManagerOperation,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await sshHostApi.post("/file_manager/pinned", file); const response = await sshHostApi.post("/file_manager/pinned", file);
return response.data; return response.data;
@@ -977,7 +1046,7 @@ export async function addFileManagerPinned(
export async function removeFileManagerPinned( export async function removeFileManagerPinned(
file: FileManagerOperation, file: FileManagerOperation,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await sshHostApi.delete("/file_manager/pinned", { const response = await sshHostApi.delete("/file_manager/pinned", {
data: file, data: file,
@@ -1003,7 +1072,7 @@ export async function getFileManagerShortcuts(
export async function addFileManagerShortcut( export async function addFileManagerShortcut(
shortcut: FileManagerOperation, shortcut: FileManagerOperation,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await sshHostApi.post("/file_manager/shortcuts", shortcut); const response = await sshHostApi.post("/file_manager/shortcuts", shortcut);
return response.data; return response.data;
@@ -1014,7 +1083,7 @@ export async function addFileManagerShortcut(
export async function removeFileManagerShortcut( export async function removeFileManagerShortcut(
shortcut: FileManagerOperation, shortcut: FileManagerOperation,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await sshHostApi.delete("/file_manager/shortcuts", { const response = await sshHostApi.delete("/file_manager/shortcuts", {
data: shortcut, data: shortcut,
@@ -1043,7 +1112,7 @@ export async function connectSSH(
credentialId?: number; credentialId?: number;
userId?: string; userId?: string;
}, },
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await fileManagerApi.post("/ssh/connect", { const response = await fileManagerApi.post("/ssh/connect", {
sessionId, sessionId,
@@ -1055,7 +1124,9 @@ export async function connectSSH(
} }
} }
export async function disconnectSSH(sessionId: string): Promise<any> { export async function disconnectSSH(
sessionId: string,
): Promise<Record<string, unknown>> {
try { try {
const response = await fileManagerApi.post("/ssh/disconnect", { const response = await fileManagerApi.post("/ssh/disconnect", {
sessionId, sessionId,
@@ -1069,7 +1140,7 @@ export async function disconnectSSH(sessionId: string): Promise<any> {
export async function verifySSHTOTP( export async function verifySSHTOTP(
sessionId: string, sessionId: string,
totpCode: string, totpCode: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await fileManagerApi.post("/ssh/connect-totp", { const response = await fileManagerApi.post("/ssh/connect-totp", {
sessionId, sessionId,
@@ -1094,7 +1165,9 @@ export async function getSSHStatus(
} }
} }
export async function keepSSHAlive(sessionId: string): Promise<any> { export async function keepSSHAlive(
sessionId: string,
): Promise<Record<string, unknown>> {
try { try {
const response = await fileManagerApi.post("/ssh/keepalive", { const response = await fileManagerApi.post("/ssh/keepalive", {
sessionId, sessionId,
@@ -1108,7 +1181,7 @@ export async function keepSSHAlive(sessionId: string): Promise<any> {
export async function listSSHFiles( export async function listSSHFiles(
sessionId: string, sessionId: string,
path: string, path: string,
): Promise<{ files: any[]; path: string }> { ): Promise<{ files: unknown[]; path: string }> {
try { try {
const response = await fileManagerApi.get("/ssh/listFiles", { const response = await fileManagerApi.get("/ssh/listFiles", {
params: { sessionId, path }, params: { sessionId, path },
@@ -1143,12 +1216,15 @@ export async function readSSHFile(
params: { sessionId, path }, params: { sessionId, path },
}); });
return response.data; return response.data;
} catch (error: any) { } catch (error: unknown) {
if (error.response?.status === 404) { if (error.response?.status === 404) {
const customError = new Error("File not found"); const customError = new Error("File not found");
(customError as any).response = error.response; (
(customError as any).isFileNotFound = customError as Error & { response?: unknown; isFileNotFound?: boolean }
error.response.data?.fileNotFound || true; ).response = error.response;
(
customError as Error & { response?: unknown; isFileNotFound?: boolean }
).isFileNotFound = error.response.data?.fileNotFound || true;
throw customError; throw customError;
} }
handleApiError(error, "read SSH file"); handleApiError(error, "read SSH file");
@@ -1161,7 +1237,7 @@ export async function writeSSHFile(
content: string, content: string,
hostId?: number, hostId?: number,
userId?: string, userId?: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await fileManagerApi.post("/ssh/writeFile", { const response = await fileManagerApi.post("/ssh/writeFile", {
sessionId, sessionId,
@@ -1192,7 +1268,7 @@ export async function uploadSSHFile(
content: string, content: string,
hostId?: number, hostId?: number,
userId?: string, userId?: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await fileManagerApi.post("/ssh/uploadFile", { const response = await fileManagerApi.post("/ssh/uploadFile", {
sessionId, sessionId,
@@ -1213,7 +1289,7 @@ export async function downloadSSHFile(
filePath: string, filePath: string,
hostId?: number, hostId?: number,
userId?: string, userId?: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await fileManagerApi.post("/ssh/downloadFile", { const response = await fileManagerApi.post("/ssh/downloadFile", {
sessionId, sessionId,
@@ -1234,7 +1310,7 @@ export async function createSSHFile(
content: string = "", content: string = "",
hostId?: number, hostId?: number,
userId?: string, userId?: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await fileManagerApi.post("/ssh/createFile", { const response = await fileManagerApi.post("/ssh/createFile", {
sessionId, sessionId,
@@ -1256,7 +1332,7 @@ export async function createSSHFolder(
folderName: string, folderName: string,
hostId?: number, hostId?: number,
userId?: string, userId?: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await fileManagerApi.post("/ssh/createFolder", { const response = await fileManagerApi.post("/ssh/createFolder", {
sessionId, sessionId,
@@ -1277,7 +1353,7 @@ export async function deleteSSHItem(
isDirectory: boolean, isDirectory: boolean,
hostId?: number, hostId?: number,
userId?: string, userId?: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await fileManagerApi.delete("/ssh/deleteItem", { const response = await fileManagerApi.delete("/ssh/deleteItem", {
data: { data: {
@@ -1300,7 +1376,7 @@ export async function copySSHItem(
targetDir: string, targetDir: string,
hostId?: number, hostId?: number,
userId?: string, userId?: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await fileManagerApi.post( const response = await fileManagerApi.post(
"/ssh/copyItem", "/ssh/copyItem",
@@ -1328,7 +1404,7 @@ export async function renameSSHItem(
newName: string, newName: string,
hostId?: number, hostId?: number,
userId?: string, userId?: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await fileManagerApi.put("/ssh/renameItem", { const response = await fileManagerApi.put("/ssh/renameItem", {
sessionId, sessionId,
@@ -1350,7 +1426,7 @@ export async function moveSSHItem(
newPath: string, newPath: string,
hostId?: number, hostId?: number,
userId?: string, userId?: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await fileManagerApi.put( const response = await fileManagerApi.put(
"/ssh/moveItem", "/ssh/moveItem",
@@ -1377,7 +1453,9 @@ export async function moveSSHItem(
// ============================================================================ // ============================================================================
// Recent Files // Recent Files
export async function getRecentFiles(hostId: number): Promise<any> { export async function getRecentFiles(
hostId: number,
): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.get("/ssh/file_manager/recent", { const response = await authApi.get("/ssh/file_manager/recent", {
params: { hostId }, params: { hostId },
@@ -1393,7 +1471,7 @@ export async function addRecentFile(
hostId: number, hostId: number,
path: string, path: string,
name?: string, name?: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.post("/ssh/file_manager/recent", { const response = await authApi.post("/ssh/file_manager/recent", {
hostId, hostId,
@@ -1410,7 +1488,7 @@ export async function addRecentFile(
export async function removeRecentFile( export async function removeRecentFile(
hostId: number, hostId: number,
path: string, path: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.delete("/ssh/file_manager/recent", { const response = await authApi.delete("/ssh/file_manager/recent", {
data: { hostId, path }, data: { hostId, path },
@@ -1422,7 +1500,9 @@ export async function removeRecentFile(
} }
} }
export async function getPinnedFiles(hostId: number): Promise<any> { export async function getPinnedFiles(
hostId: number,
): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.get("/ssh/file_manager/pinned", { const response = await authApi.get("/ssh/file_manager/pinned", {
params: { hostId }, params: { hostId },
@@ -1438,7 +1518,7 @@ export async function addPinnedFile(
hostId: number, hostId: number,
path: string, path: string,
name?: string, name?: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.post("/ssh/file_manager/pinned", { const response = await authApi.post("/ssh/file_manager/pinned", {
hostId, hostId,
@@ -1455,7 +1535,7 @@ export async function addPinnedFile(
export async function removePinnedFile( export async function removePinnedFile(
hostId: number, hostId: number,
path: string, path: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.delete("/ssh/file_manager/pinned", { const response = await authApi.delete("/ssh/file_manager/pinned", {
data: { hostId, path }, data: { hostId, path },
@@ -1467,7 +1547,9 @@ export async function removePinnedFile(
} }
} }
export async function getFolderShortcuts(hostId: number): Promise<any> { export async function getFolderShortcuts(
hostId: number,
): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.get("/ssh/file_manager/shortcuts", { const response = await authApi.get("/ssh/file_manager/shortcuts", {
params: { hostId }, params: { hostId },
@@ -1483,7 +1565,7 @@ export async function addFolderShortcut(
hostId: number, hostId: number,
path: string, path: string,
name?: string, name?: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.post("/ssh/file_manager/shortcuts", { const response = await authApi.post("/ssh/file_manager/shortcuts", {
hostId, hostId,
@@ -1500,7 +1582,7 @@ export async function addFolderShortcut(
export async function removeFolderShortcut( export async function removeFolderShortcut(
hostId: number, hostId: number,
path: string, path: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.delete("/ssh/file_manager/shortcuts", { const response = await authApi.delete("/ssh/file_manager/shortcuts", {
data: { hostId, path }, data: { hostId, path },
@@ -1552,7 +1634,7 @@ export async function getServerMetricsById(id: number): Promise<ServerMetrics> {
export async function registerUser( export async function registerUser(
username: string, username: string,
password: string, password: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.post("/users/create", { const response = await authApi.post("/users/create", {
username, username,
@@ -1638,11 +1720,11 @@ export async function getPasswordLoginAllowed(): Promise<{ allowed: boolean }> {
} }
} }
export async function getOIDCConfig(): Promise<any> { export async function getOIDCConfig(): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.get("/users/oidc-config"); const response = await authApi.get("/users/oidc-config");
return response.data; return response.data;
} catch (error: any) { } catch (error: unknown) {
console.warn( console.warn(
"Failed to fetch OIDC config:", "Failed to fetch OIDC config:",
error.response?.data?.error || error.message, error.response?.data?.error || error.message,
@@ -1669,7 +1751,9 @@ export async function getUserCount(): Promise<UserCount> {
} }
} }
export async function initiatePasswordReset(username: string): Promise<any> { export async function initiatePasswordReset(
username: string,
): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.post("/users/initiate-reset", { username }); const response = await authApi.post("/users/initiate-reset", { username });
return response.data; return response.data;
@@ -1681,7 +1765,7 @@ export async function initiatePasswordReset(username: string): Promise<any> {
export async function verifyPasswordResetCode( export async function verifyPasswordResetCode(
username: string, username: string,
resetCode: string, resetCode: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.post("/users/verify-reset-code", { const response = await authApi.post("/users/verify-reset-code", {
username, username,
@@ -1697,7 +1781,7 @@ export async function completePasswordReset(
username: string, username: string,
tempToken: string, tempToken: string,
newPassword: string, newPassword: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.post("/users/complete-reset", { const response = await authApi.post("/users/complete-reset", {
username, username,
@@ -1732,7 +1816,9 @@ export async function getUserList(): Promise<{ users: UserInfo[] }> {
} }
} }
export async function makeUserAdmin(username: string): Promise<any> { export async function makeUserAdmin(
username: string,
): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.post("/users/make-admin", { username }); const response = await authApi.post("/users/make-admin", { username });
return response.data; return response.data;
@@ -1741,7 +1827,9 @@ export async function makeUserAdmin(username: string): Promise<any> {
} }
} }
export async function removeAdminStatus(username: string): Promise<any> { export async function removeAdminStatus(
username: string,
): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.post("/users/remove-admin", { username }); const response = await authApi.post("/users/remove-admin", { username });
return response.data; return response.data;
@@ -1750,7 +1838,9 @@ export async function removeAdminStatus(username: string): Promise<any> {
} }
} }
export async function deleteUser(username: string): Promise<any> { export async function deleteUser(
username: string,
): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.delete("/users/delete-user", { const response = await authApi.delete("/users/delete-user", {
data: { username }, data: { username },
@@ -1761,7 +1851,9 @@ export async function deleteUser(username: string): Promise<any> {
} }
} }
export async function deleteAccount(password: string): Promise<any> { export async function deleteAccount(
password: string,
): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.delete("/users/delete-account", { const response = await authApi.delete("/users/delete-account", {
data: { password }, data: { password },
@@ -1774,7 +1866,7 @@ export async function deleteAccount(password: string): Promise<any> {
export async function updateRegistrationAllowed( export async function updateRegistrationAllowed(
allowed: boolean, allowed: boolean,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.patch("/users/registration-allowed", { const response = await authApi.patch("/users/registration-allowed", {
allowed, allowed,
@@ -1798,7 +1890,9 @@ export async function updatePasswordLoginAllowed(
} }
} }
export async function updateOIDCConfig(config: any): Promise<any> { export async function updateOIDCConfig(
config: Record<string, unknown>,
): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.post("/users/oidc-config", config); const response = await authApi.post("/users/oidc-config", config);
return response.data; return response.data;
@@ -1807,7 +1901,7 @@ export async function updateOIDCConfig(config: any): Promise<any> {
} }
} }
export async function disableOIDCConfig(): Promise<any> { export async function disableOIDCConfig(): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.delete("/users/oidc-config"); const response = await authApi.delete("/users/oidc-config");
return response.data; return response.data;
@@ -1893,7 +1987,9 @@ export async function generateBackupCodes(
} }
} }
export async function getUserAlerts(): Promise<{ alerts: any[] }> { export async function getUserAlerts(): Promise<{
alerts: Array<Record<string, unknown>>;
}> {
try { try {
const response = await authApi.get(`/alerts`); const response = await authApi.get(`/alerts`);
return response.data; return response.data;
@@ -1902,7 +1998,9 @@ export async function getUserAlerts(): Promise<{ alerts: any[] }> {
} }
} }
export async function dismissAlert(alertId: string): Promise<any> { export async function dismissAlert(
alertId: string,
): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.post("/alerts/dismiss", { alertId }); const response = await authApi.post("/alerts/dismiss", { alertId });
return response.data; return response.data;
@@ -1915,7 +2013,9 @@ export async function dismissAlert(alertId: string): Promise<any> {
// UPDATES & RELEASES // UPDATES & RELEASES
// ============================================================================ // ============================================================================
export async function getReleasesRSS(perPage: number = 100): Promise<any> { export async function getReleasesRSS(
perPage: number = 100,
): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.get(`/releases/rss?per_page=${perPage}`); const response = await authApi.get(`/releases/rss?per_page=${perPage}`);
return response.data; return response.data;
@@ -1924,7 +2024,7 @@ export async function getReleasesRSS(perPage: number = 100): Promise<any> {
} }
} }
export async function getVersionInfo(): Promise<any> { export async function getVersionInfo(): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.get("/version"); const response = await authApi.get("/version");
return response.data; return response.data;
@@ -1937,7 +2037,7 @@ export async function getVersionInfo(): Promise<any> {
// DATABASE HEALTH // DATABASE HEALTH
// ============================================================================ // ============================================================================
export async function getDatabaseHealth(): Promise<any> { export async function getDatabaseHealth(): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.get("/users/db-health"); const response = await authApi.get("/users/db-health");
return response.data; return response.data;
@@ -1950,7 +2050,7 @@ export async function getDatabaseHealth(): Promise<any> {
// SSH CREDENTIALS MANAGEMENT // SSH CREDENTIALS MANAGEMENT
// ============================================================================ // ============================================================================
export async function getCredentials(): Promise<any> { export async function getCredentials(): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.get("/credentials"); const response = await authApi.get("/credentials");
return response.data; return response.data;
@@ -1959,7 +2059,9 @@ export async function getCredentials(): Promise<any> {
} }
} }
export async function getCredentialDetails(credentialId: number): Promise<any> { export async function getCredentialDetails(
credentialId: number,
): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.get(`/credentials/${credentialId}`); const response = await authApi.get(`/credentials/${credentialId}`);
return response.data; return response.data;
@@ -1968,7 +2070,9 @@ export async function getCredentialDetails(credentialId: number): Promise<any> {
} }
} }
export async function createCredential(credentialData: any): Promise<any> { export async function createCredential(
credentialData: Record<string, unknown>,
): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.post("/credentials", credentialData); const response = await authApi.post("/credentials", credentialData);
return response.data; return response.data;
@@ -1979,8 +2083,8 @@ export async function createCredential(credentialData: any): Promise<any> {
export async function updateCredential( export async function updateCredential(
credentialId: number, credentialId: number,
credentialData: any, credentialData: Record<string, unknown>,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.put( const response = await authApi.put(
`/credentials/${credentialId}`, `/credentials/${credentialId}`,
@@ -1992,7 +2096,9 @@ export async function updateCredential(
} }
} }
export async function deleteCredential(credentialId: number): Promise<any> { export async function deleteCredential(
credentialId: number,
): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.delete(`/credentials/${credentialId}`); const response = await authApi.delete(`/credentials/${credentialId}`);
return response.data; return response.data;
@@ -2001,7 +2107,9 @@ export async function deleteCredential(credentialId: number): Promise<any> {
} }
} }
export async function getCredentialHosts(credentialId: number): Promise<any> { export async function getCredentialHosts(
credentialId: number,
): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.get(`/credentials/${credentialId}/hosts`); const response = await authApi.get(`/credentials/${credentialId}/hosts`);
return response.data; return response.data;
@@ -2010,7 +2118,7 @@ export async function getCredentialHosts(credentialId: number): Promise<any> {
} }
} }
export async function getCredentialFolders(): Promise<any> { export async function getCredentialFolders(): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.get("/credentials/folders"); const response = await authApi.get("/credentials/folders");
return response.data; return response.data;
@@ -2019,7 +2127,9 @@ export async function getCredentialFolders(): Promise<any> {
} }
} }
export async function getSSHHostWithCredentials(hostId: number): Promise<any> { export async function getSSHHostWithCredentials(
hostId: number,
): Promise<Record<string, unknown>> {
try { try {
const response = await sshHostApi.get( const response = await sshHostApi.get(
`/db/host/${hostId}/with-credentials`, `/db/host/${hostId}/with-credentials`,
@@ -2033,7 +2143,7 @@ export async function getSSHHostWithCredentials(hostId: number): Promise<any> {
export async function applyCredentialToHost( export async function applyCredentialToHost(
hostId: number, hostId: number,
credentialId: number, credentialId: number,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await sshHostApi.post( const response = await sshHostApi.post(
`/db/host/${hostId}/apply-credential`, `/db/host/${hostId}/apply-credential`,
@@ -2045,7 +2155,9 @@ export async function applyCredentialToHost(
} }
} }
export async function removeCredentialFromHost(hostId: number): Promise<any> { export async function removeCredentialFromHost(
hostId: number,
): Promise<Record<string, unknown>> {
try { try {
const response = await sshHostApi.delete(`/db/host/${hostId}/credential`); const response = await sshHostApi.delete(`/db/host/${hostId}/credential`);
return response.data; return response.data;
@@ -2057,7 +2169,7 @@ export async function removeCredentialFromHost(hostId: number): Promise<any> {
export async function migrateHostToCredential( export async function migrateHostToCredential(
hostId: number, hostId: number,
credentialName: string, credentialName: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await sshHostApi.post( const response = await sshHostApi.post(
`/db/host/${hostId}/migrate-to-credential`, `/db/host/${hostId}/migrate-to-credential`,
@@ -2073,7 +2185,7 @@ export async function migrateHostToCredential(
// SSH FOLDER MANAGEMENT // SSH FOLDER MANAGEMENT
// ============================================================================ // ============================================================================
export async function getFoldersWithStats(): Promise<any> { export async function getFoldersWithStats(): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.get("/ssh/db/folders/with-stats"); const response = await authApi.get("/ssh/db/folders/with-stats");
return response.data; return response.data;
@@ -2085,7 +2197,7 @@ export async function getFoldersWithStats(): Promise<any> {
export async function renameFolder( export async function renameFolder(
oldName: string, oldName: string,
newName: string, newName: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.put("/ssh/folders/rename", { const response = await authApi.put("/ssh/folders/rename", {
oldName, oldName,
@@ -2100,7 +2212,7 @@ export async function renameFolder(
export async function renameCredentialFolder( export async function renameCredentialFolder(
oldName: string, oldName: string,
newName: string, newName: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.put("/credentials/folders/rename", { const response = await authApi.put("/credentials/folders/rename", {
oldName, oldName,
@@ -2115,7 +2227,7 @@ export async function renameCredentialFolder(
export async function detectKeyType( export async function detectKeyType(
privateKey: string, privateKey: string,
keyPassword?: string, keyPassword?: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.post("/credentials/detect-key-type", { const response = await authApi.post("/credentials/detect-key-type", {
privateKey, privateKey,
@@ -2127,7 +2239,9 @@ export async function detectKeyType(
} }
} }
export async function detectPublicKeyType(publicKey: string): Promise<any> { export async function detectPublicKeyType(
publicKey: string,
): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.post("/credentials/detect-public-key-type", { const response = await authApi.post("/credentials/detect-public-key-type", {
publicKey, publicKey,
@@ -2142,7 +2256,7 @@ export async function validateKeyPair(
privateKey: string, privateKey: string,
publicKey: string, publicKey: string,
keyPassword?: string, keyPassword?: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.post("/credentials/validate-key-pair", { const response = await authApi.post("/credentials/validate-key-pair", {
privateKey, privateKey,
@@ -2158,7 +2272,7 @@ export async function validateKeyPair(
export async function generatePublicKeyFromPrivate( export async function generatePublicKeyFromPrivate(
privateKey: string, privateKey: string,
keyPassword?: string, keyPassword?: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.post("/credentials/generate-public-key", { const response = await authApi.post("/credentials/generate-public-key", {
privateKey, privateKey,
@@ -2174,7 +2288,7 @@ export async function generateKeyPair(
keyType: "ssh-ed25519" | "ssh-rsa" | "ecdsa-sha2-nistp256", keyType: "ssh-ed25519" | "ssh-rsa" | "ecdsa-sha2-nistp256",
keySize?: number, keySize?: number,
passphrase?: string, passphrase?: string,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.post("/credentials/generate-key-pair", { const response = await authApi.post("/credentials/generate-key-pair", {
keyType, keyType,
@@ -2190,7 +2304,7 @@ export async function generateKeyPair(
export async function deployCredentialToHost( export async function deployCredentialToHost(
credentialId: number, credentialId: number,
targetHostId: number, targetHostId: number,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.post( const response = await authApi.post(
`/credentials/${credentialId}/deploy-to-host`, `/credentials/${credentialId}/deploy-to-host`,
@@ -2206,7 +2320,7 @@ export async function deployCredentialToHost(
// SNIPPETS API // SNIPPETS API
// ============================================================================ // ============================================================================
export async function getSnippets(): Promise<any> { export async function getSnippets(): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.get("/snippets"); const response = await authApi.get("/snippets");
return response.data; return response.data;
@@ -2215,7 +2329,9 @@ export async function getSnippets(): Promise<any> {
} }
} }
export async function createSnippet(snippetData: any): Promise<any> { export async function createSnippet(
snippetData: Record<string, unknown>,
): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.post("/snippets", snippetData); const response = await authApi.post("/snippets", snippetData);
return response.data; return response.data;
@@ -2226,8 +2342,8 @@ export async function createSnippet(snippetData: any): Promise<any> {
export async function updateSnippet( export async function updateSnippet(
snippetId: number, snippetId: number,
snippetData: any, snippetData: Record<string, unknown>,
): Promise<any> { ): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.put(`/snippets/${snippetId}`, snippetData); const response = await authApi.put(`/snippets/${snippetId}`, snippetData);
return response.data; return response.data;
@@ -2236,7 +2352,9 @@ export async function updateSnippet(
} }
} }
export async function deleteSnippet(snippetId: number): Promise<any> { export async function deleteSnippet(
snippetId: number,
): Promise<Record<string, unknown>> {
try { try {
const response = await authApi.delete(`/snippets/${snippetId}`); const response = await authApi.delete(`/snippets/${snippetId}`);
return response.data; return response.data;