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

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

View File

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

medium

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

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

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