fix: replace explicit any types with proper TypeScript types

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 {
getAppVersion: () => Promise<string>;
getPlatform: () => Promise<string>;
getServerConfig: () => Promise<any>;
saveServerConfig: (config: any) => Promise<any>;
testServerConnection: (serverUrl: string) => Promise<any>;
getServerConfig: () => Promise<ServerConfig>;
saveServerConfig: (config: ServerConfig) => Promise<{ success: boolean }>;
testServerConnection: (serverUrl: string) => Promise<ConnectionTestResult>;
showSaveDialog: (options: any) => Promise<any>;
showOpenDialog: (options: any) => Promise<any>;
showSaveDialog: (options: DialogOptions) => Promise<DialogResult>;
showOpenDialog: (options: DialogOptions) => Promise<DialogResult>;
onUpdateAvailable: (callback: Function) => void;
onUpdateDownloaded: (callback: Function) => void;
onUpdateAvailable: (callback: () => void) => void;
onUpdateDownloaded: (callback: () => void) => void;
removeAllListeners: (channel: string) => void;
isElectron: boolean;
isDev: boolean;
invoke: (channel: string, ...args: any[]) => Promise<any>;
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>;
createTempFile: (fileData: {
fileName: string;

View File

@@ -4,6 +4,7 @@
// This file contains all shared interfaces and types used across the application
import type { Client } from "ssh2";
import type { Request } from "express";
// ============================================================================
// SSH HOST TYPES
@@ -58,7 +59,7 @@ export interface SSHHostData {
enableTunnel?: boolean;
enableFileManager?: boolean;
defaultPath?: string;
tunnelConnections?: any[];
tunnelConnections?: TunnelConnection[];
statsConfig?: string;
}
@@ -263,8 +264,8 @@ export interface TabContextTab {
| "file_manager"
| "user_profile";
title: string;
hostConfig?: any;
terminalRef?: React.RefObject<any>;
hostConfig?: SSHHost;
terminalRef?: React.RefObject<HTMLElement>;
}
// ============================================================================
@@ -305,7 +306,7 @@ export type KeyType = "rsa" | "ecdsa" | "ed25519";
// API RESPONSE TYPES
// ============================================================================
export interface ApiResponse<T = any> {
export interface ApiResponse<T = unknown> {
data?: T;
error?: string;
message?: string;
@@ -368,13 +369,13 @@ export interface SSHTunnelViewerProps {
action: "connect" | "disconnect" | "cancel",
host: SSHHost,
tunnelIndex: number,
) => Promise<any>
) => Promise<void>
>;
onTunnelAction?: (
action: "connect" | "disconnect" | "cancel",
host: SSHHost,
tunnelIndex: number,
) => Promise<any>;
) => Promise<void>;
}
export interface FileManagerProps {
@@ -402,7 +403,7 @@ export interface SSHTunnelObjectProps {
action: "connect" | "disconnect" | "cancel",
host: SSHHost,
tunnelIndex: number,
) => Promise<any>;
) => Promise<void>;
compact?: 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 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(() => {
if (isElectron()) {
const serverUrl = (window as any).configuredServerUrl;
const serverUrl = (window as { configuredServerUrl?: string })
.configuredServerUrl;
if (!serverUrl) {
return;
}
@@ -127,7 +128,8 @@ export function AdminSettings({
React.useEffect(() => {
if (isElectron()) {
const serverUrl = (window as any).configuredServerUrl;
const serverUrl = (window as { configuredServerUrl?: string })
.configuredServerUrl;
if (!serverUrl) {
return;
}
@@ -148,7 +150,8 @@ export function AdminSettings({
React.useEffect(() => {
if (isElectron()) {
const serverUrl = (window as any).configuredServerUrl;
const serverUrl = (window as { configuredServerUrl?: string })
.configuredServerUrl;
if (!serverUrl) {
return;
}
@@ -169,7 +172,8 @@ export function AdminSettings({
const fetchUsers = async () => {
if (isElectron()) {
const serverUrl = (window as any).configuredServerUrl;
const serverUrl = (window as { configuredServerUrl?: string })
.configuredServerUrl;
if (!serverUrl) {
return;
}
@@ -234,9 +238,10 @@ export function AdminSettings({
try {
await updateOIDCConfig(oidcConfig);
toast.success(t("admin.oidcConfigurationUpdated"));
} catch (err: any) {
} catch (err: unknown) {
setOidcError(
err?.response?.data?.error || t("admin.failedToUpdateOidcConfig"),
(err as { response?: { data?: { error?: string } } })?.response?.data
?.error || t("admin.failedToUpdateOidcConfig"),
);
} finally {
setOidcLoading(false);
@@ -257,9 +262,10 @@ export function AdminSettings({
toast.success(t("admin.userIsNowAdmin", { username: newAdminUsername }));
setNewAdminUsername("");
fetchUsers();
} catch (err: any) {
} catch (err: unknown) {
setMakeAdminError(
err?.response?.data?.error || t("admin.failedToMakeUserAdmin"),
(err as { response?: { data?: { error?: string } } })?.response?.data
?.error || t("admin.failedToMakeUserAdmin"),
);
} finally {
setMakeAdminLoading(false);
@@ -272,7 +278,7 @@ export function AdminSettings({
await removeAdminStatus(username);
toast.success(t("admin.adminStatusRemoved", { username }));
fetchUsers();
} catch (err: any) {
} catch (err: unknown) {
toast.error(t("admin.failedToRemoveAdminStatus"));
}
});
@@ -286,7 +292,7 @@ export function AdminSettings({
await deleteUser(username);
toast.success(t("admin.userDeletedSuccessfully", { username }));
fetchUsers();
} catch (err: any) {
} catch (err: unknown) {
toast.error(t("admin.failedToDeleteUser"));
}
},
@@ -316,7 +322,7 @@ export function AdminSettings({
window.location.hostname === "127.0.0.1");
const apiUrl = isElectron()
? `${(window as any).configuredServerUrl}/database/export`
? `${(window as { configuredServerUrl?: string }).configuredServerUrl}/database/export`
: isDev
? `http://localhost:30001/database/export`
: `${window.location.protocol}//${window.location.host}/database/export`;
@@ -386,7 +392,7 @@ export function AdminSettings({
window.location.hostname === "127.0.0.1");
const apiUrl = isElectron()
? `${(window as any).configuredServerUrl}/database/import`
? `${(window as { configuredServerUrl?: string }).configuredServerUrl}/database/import`
: isDev
? `http://localhost:30001/database/import`
: `${window.location.protocol}//${window.location.host}/database/import`;
@@ -713,9 +719,13 @@ export function AdminSettings({
try {
await disableOIDCConfig();
toast.success(t("admin.oidcConfigurationDisabled"));
} catch (err: any) {
} catch (err: unknown) {
setOidcError(
err?.response?.data?.error ||
(
err as {
response?: { data?: { error?: string } };
}
)?.response?.data?.error ||
t("admin.failedToDisableOidcConfig"),
);
} finally {

View File

@@ -311,10 +311,10 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
setFiles(files);
clearSelection();
initialLoadDoneRef.current = true;
} catch (dirError: any) {
} catch (dirError: unknown) {
console.error("Failed to load initial directory:", dirError);
}
} catch (error: any) {
} catch (error: unknown) {
console.error("SSH connection failed:", error);
handleCloseWithError(
t("fileManager.failedToConnect") + ": " + (error.message || error),
@@ -353,7 +353,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
setFiles(files);
clearSelection();
} catch (error: any) {
} catch (error: unknown) {
if (currentLoadingPathRef.current === path) {
console.error("Failed to load directory:", error);
@@ -535,7 +535,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
t("fileManager.fileUploadedSuccessfully", { name: file.name }),
);
handleRefreshDirectory();
} catch (error: any) {
} catch (error: unknown) {
toast.dismiss(progressToast);
if (
@@ -584,7 +584,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
t("fileManager.fileDownloadedSuccessfully", { name: file.name }),
);
}
} catch (error: any) {
} catch (error: unknown) {
if (
error.message?.includes("connection") ||
error.message?.includes("established")
@@ -665,7 +665,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
);
handleRefreshDirectory();
clearSelection();
} catch (error: any) {
} catch (error: unknown) {
if (
error.message?.includes("connection") ||
error.message?.includes("established")
@@ -775,7 +775,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
component: createWindowComponent,
});
}
} catch (error: any) {
} catch (error: unknown) {
toast.error(
error?.response?.data?.error ||
error?.message ||
@@ -914,7 +914,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
successCount++;
}
}
} catch (error: any) {
} catch (error: unknown) {
console.error(`Failed to ${operation} file ${file.name}:`, error);
toast.error(
t("fileManager.operationFailed", {
@@ -1015,7 +1015,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
if (operation === "cut") {
setClipboard(null);
}
} catch (error: any) {
} catch (error: unknown) {
toast.error(
`${t("fileManager.pasteFailed")}: ${error.message || t("fileManager.unknownError")}`,
);
@@ -1050,7 +1050,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
currentHost?.userId?.toString(),
);
successCount++;
} catch (error: any) {
} catch (error: unknown) {
console.error(
`Failed to delete copied file ${copiedFile.targetName}:`,
error,
@@ -1092,7 +1092,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
currentHost?.userId?.toString(),
);
successCount++;
} catch (error: any) {
} catch (error: unknown) {
console.error(
`Failed to move back file ${movedFile.targetName}:`,
error,
@@ -1132,7 +1132,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
}
handleRefreshDirectory();
} catch (error: any) {
} catch (error: unknown) {
toast.error(
`${t("fileManager.undoOperationFailed")}: ${error.message || t("fileManager.unknownError")}`,
);
@@ -1204,7 +1204,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
setCreateIntent(null);
handleRefreshDirectory();
} catch (error: any) {
} catch (error: unknown) {
console.error("Create failed:", error);
toast.error(t("fileManager.failedToCreateItem"));
}
@@ -1233,7 +1233,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
);
setEditingFile(null);
handleRefreshDirectory();
} catch (error: any) {
} catch (error: unknown) {
console.error("Rename failed:", error);
toast.error(t("fileManager.failedToRenameItem"));
}
@@ -1269,11 +1269,11 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
clearSelection();
initialLoadDoneRef.current = true;
toast.success(t("fileManager.connectedSuccessfully"));
} catch (dirError: any) {
} catch (dirError: unknown) {
console.error("Failed to load initial directory:", dirError);
}
}
} catch (error: any) {
} catch (error: unknown) {
console.error("TOTP verification failed:", error);
toast.error(t("fileManager.totpVerificationFailed"));
} finally {
@@ -1340,7 +1340,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
movedItems.push(file.name);
successCount++;
}
} catch (error: any) {
} catch (error: unknown) {
console.error(`Failed to move file ${file.name}:`, error);
toast.error(
t("fileManager.moveFileFailed", { name: file.name }) +
@@ -1388,7 +1388,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
handleRefreshDirectory();
clearSelection();
}
} catch (error: any) {
} catch (error: unknown) {
console.error("Drag move operation failed:", error);
toast.error(t("fileManager.moveOperationFailed") + ": " + error.message);
}
@@ -1459,7 +1459,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
await dragToDesktop.dragFilesToDesktop(files);
}
}
} catch (error: any) {
} catch (error: unknown) {
console.error("Drag to desktop failed:", error);
toast.error(
t("fileManager.dragFailed") +
@@ -1554,7 +1554,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
try {
const pinnedData = await getPinnedFiles(currentHost.id);
const pinnedPaths = new Set(pinnedData.map((item: any) => item.path));
const pinnedPaths = new Set(
pinnedData.map((item: Record<string, unknown>) => item.path),
);
setPinnedFiles(pinnedPaths);
} catch (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";
import { Button } from "@/components/ui/button.tsx";
interface TabData {
id: number;
type: string;
title: string;
terminalRef?: {
current?: {
fit?: () => void;
notifyResize?: () => void;
refresh?: () => void;
};
};
hostConfig?: unknown;
[key: string]: unknown;
}
interface TerminalViewProps {
isTopbarOpen?: boolean;
}
@@ -25,11 +40,16 @@ interface TerminalViewProps {
export function AppView({
isTopbarOpen = true,
}: TerminalViewProps): React.ReactElement {
const { tabs, currentTab, allSplitScreenTab, removeTab } = useTabs() as any;
const { tabs, currentTab, allSplitScreenTab, removeTab } = useTabs() as {
tabs: TabData[];
currentTab: number;
allSplitScreenTab: number[];
removeTab: (id: number) => void;
};
const { state: sidebarState } = useSidebar();
const terminalTabs = tabs.filter(
(tab: any) =>
(tab: TabData) =>
tab.type === "terminal" ||
tab.type === "server" ||
tab.type === "file_manager",
@@ -59,7 +79,7 @@ export function AppView({
const splitIds = allSplitScreenTab as number[];
visibleIds.push(currentTab, ...splitIds.filter((i) => i !== currentTab));
}
terminalTabs.forEach((t: any) => {
terminalTabs.forEach((t: TabData) => {
if (visibleIds.includes(t.id)) {
const ref = t.terminalRef?.current;
if (ref?.fit) ref.fit();
@@ -125,16 +145,16 @@ export function AppView({
const renderTerminalsLayer = () => {
const styles: Record<number, React.CSSProperties> = {};
const splitTabs = terminalTabs.filter((tab: any) =>
const splitTabs = terminalTabs.filter((tab: TabData) =>
allSplitScreenTab.includes(tab.id),
);
const mainTab = terminalTabs.find((tab: any) => tab.id === currentTab);
const mainTab = terminalTabs.find((tab: TabData) => tab.id === currentTab);
const layoutTabs = [
mainTab,
...splitTabs.filter(
(t: any) => t && t.id !== (mainTab && (mainTab as any).id),
(t: TabData) => t && t.id !== (mainTab && (mainTab as TabData).id),
),
].filter(Boolean) as any[];
].filter((t): t is TabData => t !== null && t !== undefined);
if (allSplitScreenTab.length === 0 && mainTab) {
const isFileManagerTab = mainTab.type === "file_manager";
@@ -150,7 +170,7 @@ export function AppView({
opacity: ready ? 1 : 0,
};
} else {
layoutTabs.forEach((t: any) => {
layoutTabs.forEach((t: TabData) => {
const rect = panelRects[String(t.id)];
const parentRect = containerRef.current?.getBoundingClientRect();
if (rect && parentRect) {
@@ -171,7 +191,7 @@ export function AppView({
return (
<div className="absolute inset-0 z-[1]">
{terminalTabs.map((t: any) => {
{terminalTabs.map((t: TabData) => {
const hasStyle = !!styles[t.id];
const isVisible =
hasStyle || (allSplitScreenTab.length === 0 && t.id === currentTab);
@@ -241,16 +261,16 @@ export function AppView({
};
const renderSplitOverlays = () => {
const splitTabs = terminalTabs.filter((tab: any) =>
const splitTabs = terminalTabs.filter((tab: TabData) =>
allSplitScreenTab.includes(tab.id),
);
const mainTab = terminalTabs.find((tab: any) => tab.id === currentTab);
const mainTab = terminalTabs.find((tab: TabData) => tab.id === currentTab);
const layoutTabs = [
mainTab,
...splitTabs.filter(
(t: any) => t && t.id !== (mainTab && (mainTab as any).id),
(t: TabData) => t && t.id !== (mainTab && (mainTab as TabData).id),
),
].filter(Boolean) as any[];
].filter((t): t is TabData => t !== null && t !== undefined);
if (allSplitScreenTab.length === 0) return null;
const handleStyle = {
@@ -258,13 +278,16 @@ export function AppView({
zIndex: 12,
background: "var(--color-dark-border)",
} as React.CSSProperties;
const commonGroupProps = {
const commonGroupProps: {
onLayout: () => void;
onResize: () => void;
} = {
onLayout: scheduleMeasureAndFit,
onResize: scheduleMeasureAndFit,
} as any;
};
if (layoutTabs.length === 2) {
const [a, b] = layoutTabs as any[];
const [a, b] = layoutTabs;
return (
<div className="absolute inset-0 z-[10] pointer-events-none">
<ResizablePrimitive.PanelGroup
@@ -316,7 +339,7 @@ export function AppView({
);
}
if (layoutTabs.length === 3) {
const [a, b, c] = layoutTabs as any[];
const [a, b, c] = layoutTabs;
return (
<div className="absolute inset-0 z-[10] pointer-events-none">
<ResizablePrimitive.PanelGroup
@@ -404,7 +427,7 @@ export function AppView({
);
}
if (layoutTabs.length === 4) {
const [a, b, c, d] = layoutTabs as any[];
const [a, b, c, d] = layoutTabs;
return (
<div className="absolute inset-0 z-[10] pointer-events-none">
<ResizablePrimitive.PanelGroup
@@ -529,7 +552,7 @@ export function AppView({
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 isSplitScreen = allSplitScreenTab.length > 0;

View File

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

View File

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

View File

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

View File

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