feat: enhance server stats widgets and fix TypeScript/ESLint errors #394

Merged
ZacharyZcR merged 50 commits from feature-server-stats-customization into dev-1.8.0 2025-10-10 03:48:34 +00:00
22 changed files with 2002 additions and 1540 deletions
Showing only changes of commit d7e98cda04 - Show all commits

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"
gemini-code-assist[bot] commented 2025-10-09 15:36:39 +00:00 (Migrated from github.com)
Review

medium

The type for terminalRef in the TabContextTab interface has been changed to React.RefObject<HTMLElement>, which is incorrect. This ref is used for an imperative handle on the Terminal component, which exposes methods like fit() and sendInput(). Using HTMLElement loses type safety and could lead to runtime errors.

A TerminalHandle interface is defined in src/backend/ssh/terminal.ts. It would be best to move this interface to a shared types file (like this one) and use React.RefObject<TerminalHandle> here and in other related components.

  terminalRef?: React.RefObject<TerminalHandle>;
![medium](https://www.gstatic.com/codereviewagent/medium-priority.svg) The type for `terminalRef` in the `TabContextTab` interface has been changed to `React.RefObject<HTMLElement>`, which is incorrect. This ref is used for an imperative handle on the `Terminal` component, which exposes methods like `fit()` and `sendInput()`. Using `HTMLElement` loses type safety and could lead to runtime errors. A `TerminalHandle` interface is defined in `src/backend/ssh/terminal.ts`. It would be best to move this interface to a shared types file (like this one) and use `React.RefObject<TerminalHandle>` here and in other related components. ```suggestion terminalRef?: React.RefObject<TerminalHandle>; ```
| "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;