fix: replace explicit any types with proper TypeScript types

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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