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:
@@ -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, {
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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(() => ""));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user