fix: replace explicit any types with proper TypeScript types
- Create explicit interfaces for Request extensions (AuthenticatedRequest, RequestWithHeaders) - Add type definitions for WebSocket messages and SSH connection data - Use generic types in DataCrypto methods instead of any return types - Define proper interfaces for file manager data structures - Replace catch block any types with unknown and proper type assertions - Add HostConfig and TabData interfaces for Server component Fixes 32 @typescript-eslint/no-explicit-any violations across 5 files
This commit is contained in:
@@ -1,14 +1,55 @@
|
|||||||
import { WebSocketServer, WebSocket, type RawData } from "ws";
|
import { WebSocketServer, WebSocket, type RawData } from "ws";
|
||||||
import { Client, type ClientChannel, type PseudoTtyOptions } from "ssh2";
|
import {
|
||||||
|
Client,
|
||||||
|
type ClientChannel,
|
||||||
|
type PseudoTtyOptions,
|
||||||
|
type ConnectConfig,
|
||||||
|
} from "ssh2";
|
||||||
import { parse as parseUrl } from "url";
|
import { parse as parseUrl } from "url";
|
||||||
import { getDb } from "../database/db/index.js";
|
import { getDb } from "../database/db/index.js";
|
||||||
import { sshCredentials } from "../database/db/schema.js";
|
import { sshCredentials } from "../database/db/schema.js";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { sshLogger } from "../utils/logger.js";
|
import { sshLogger } from "../utils/logger.js";
|
||||||
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
||||||
import { AuthManager } from "../utils/auth-manager.js";
|
import { AuthManager, type JWTPayload } from "../utils/auth-manager.js";
|
||||||
import { UserCrypto } from "../utils/user-crypto.js";
|
import { UserCrypto } from "../utils/user-crypto.js";
|
||||||
|
|
||||||
|
interface ConnectToHostData {
|
||||||
|
cols: number;
|
||||||
|
rows: number;
|
||||||
|
hostConfig: {
|
||||||
|
id: number;
|
||||||
|
ip: string;
|
||||||
|
port: number;
|
||||||
|
username: string;
|
||||||
|
password?: string;
|
||||||
|
key?: string;
|
||||||
|
keyPassword?: string;
|
||||||
|
keyType?: string;
|
||||||
|
authType?: string;
|
||||||
|
credentialId?: number;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
initialPath?: string;
|
||||||
|
executeCommand?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResizeData {
|
||||||
|
cols: number;
|
||||||
|
rows: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TOTPResponseData {
|
||||||
|
code?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WebSocketMessage {
|
||||||
|
type: string;
|
||||||
|
data?: ConnectToHostData | ResizeData | TOTPResponseData | string | unknown;
|
||||||
|
code?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
const authManager = AuthManager.getInstance();
|
const authManager = AuthManager.getInstance();
|
||||||
const userCrypto = UserCrypto.getInstance();
|
const userCrypto = UserCrypto.getInstance();
|
||||||
|
|
||||||
@@ -79,7 +120,7 @@ const wss = new WebSocketServer({
|
|||||||
|
|
||||||
wss.on("connection", async (ws: WebSocket, req) => {
|
wss.on("connection", async (ws: WebSocket, req) => {
|
||||||
let userId: string | undefined;
|
let userId: string | undefined;
|
||||||
let userPayload: any;
|
let userPayload: JWTPayload | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = parseUrl(req.url!, true);
|
const url = parseUrl(req.url!, true);
|
||||||
@@ -187,9 +228,9 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let parsed: any;
|
let parsed: WebSocketMessage;
|
||||||
try {
|
try {
|
||||||
parsed = JSON.parse(msg.toString());
|
parsed = JSON.parse(msg.toString()) as WebSocketMessage;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
sshLogger.error("Invalid JSON received", e, {
|
sshLogger.error("Invalid JSON received", e, {
|
||||||
operation: "websocket_message_invalid_json",
|
operation: "websocket_message_invalid_json",
|
||||||
@@ -203,16 +244,17 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
const { type, data } = parsed;
|
const { type, data } = parsed;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "connectToHost":
|
case "connectToHost": {
|
||||||
if (data.hostConfig) {
|
const connectData = data as ConnectToHostData;
|
||||||
data.hostConfig.userId = userId;
|
if (connectData.hostConfig) {
|
||||||
|
connectData.hostConfig.userId = userId;
|
||||||
}
|
}
|
||||||
handleConnectToHost(data).catch((error) => {
|
handleConnectToHost(connectData).catch((error) => {
|
||||||
sshLogger.error("Failed to connect to host", error, {
|
sshLogger.error("Failed to connect to host", error, {
|
||||||
operation: "ssh_connect",
|
operation: "ssh_connect",
|
||||||
userId,
|
userId,
|
||||||
hostId: data.hostConfig?.id,
|
hostId: connectData.hostConfig?.id,
|
||||||
ip: data.hostConfig?.ip,
|
ip: connectData.hostConfig?.ip,
|
||||||
});
|
});
|
||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@@ -224,43 +266,52 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case "resize":
|
case "resize": {
|
||||||
handleResize(data);
|
const resizeData = data as ResizeData;
|
||||||
|
handleResize(resizeData);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case "disconnect":
|
case "disconnect":
|
||||||
cleanupSSH();
|
cleanupSSH();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "input":
|
case "input": {
|
||||||
|
const inputData = data as string;
|
||||||
if (sshStream) {
|
if (sshStream) {
|
||||||
if (data === "\t") {
|
if (inputData === "\t") {
|
||||||
sshStream.write(data);
|
sshStream.write(inputData);
|
||||||
} else if (data.startsWith("\x1b")) {
|
} else if (
|
||||||
sshStream.write(data);
|
typeof inputData === "string" &&
|
||||||
|
inputData.startsWith("\x1b")
|
||||||
|
) {
|
||||||
|
sshStream.write(inputData);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
sshStream.write(Buffer.from(data, "utf8"));
|
sshStream.write(Buffer.from(inputData, "utf8"));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sshLogger.error("Error writing input to SSH stream", error, {
|
sshLogger.error("Error writing input to SSH stream", error, {
|
||||||
operation: "ssh_input_encoding",
|
operation: "ssh_input_encoding",
|
||||||
userId,
|
userId,
|
||||||
dataLength: data.length,
|
dataLength: inputData.length,
|
||||||
});
|
});
|
||||||
sshStream.write(Buffer.from(data, "latin1"));
|
sshStream.write(Buffer.from(inputData, "latin1"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case "ping":
|
case "ping":
|
||||||
ws.send(JSON.stringify({ type: "pong" }));
|
ws.send(JSON.stringify({ type: "pong" }));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "totp_response":
|
case "totp_response": {
|
||||||
if (keyboardInteractiveFinish && data?.code) {
|
const totpData = data as TOTPResponseData;
|
||||||
const totpCode = data.code;
|
if (keyboardInteractiveFinish && totpData?.code) {
|
||||||
|
const totpCode = totpData.code;
|
||||||
sshLogger.info("TOTP code received from user", {
|
sshLogger.info("TOTP code received from user", {
|
||||||
operation: "totp_response",
|
operation: "totp_response",
|
||||||
userId,
|
userId,
|
||||||
@@ -274,7 +325,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
operation: "totp_response_error",
|
operation: "totp_response_error",
|
||||||
userId,
|
userId,
|
||||||
hasCallback: !!keyboardInteractiveFinish,
|
hasCallback: !!keyboardInteractiveFinish,
|
||||||
hasCode: !!data?.code,
|
hasCode: !!totpData?.code,
|
||||||
});
|
});
|
||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@@ -284,6 +335,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
sshLogger.warn("Unknown message type received", {
|
sshLogger.warn("Unknown message type received", {
|
||||||
@@ -294,25 +346,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleConnectToHost(data: {
|
async function handleConnectToHost(data: ConnectToHostData) {
|
||||||
cols: number;
|
|
||||||
rows: number;
|
|
||||||
hostConfig: {
|
|
||||||
id: number;
|
|
||||||
ip: string;
|
|
||||||
port: number;
|
|
||||||
username: string;
|
|
||||||
password?: string;
|
|
||||||
key?: string;
|
|
||||||
keyPassword?: string;
|
|
||||||
keyType?: string;
|
|
||||||
authType?: string;
|
|
||||||
credentialId?: number;
|
|
||||||
userId?: string;
|
|
||||||
};
|
|
||||||
initialPath?: string;
|
|
||||||
executeCommand?: string;
|
|
||||||
}) {
|
|
||||||
const { cols, rows, hostConfig, initialPath, executeCommand } = data;
|
const { cols, rows, hostConfig, initialPath, executeCommand } = data;
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
@@ -642,7 +676,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const connectConfig: any = {
|
const connectConfig: ConnectConfig = {
|
||||||
host: ip,
|
host: ip,
|
||||||
port,
|
port,
|
||||||
username,
|
username,
|
||||||
@@ -650,21 +684,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
keepaliveInterval: 30000,
|
keepaliveInterval: 30000,
|
||||||
keepaliveCountMax: 3,
|
keepaliveCountMax: 3,
|
||||||
readyTimeout: 60000,
|
readyTimeout: 60000,
|
||||||
tcpKeepAlive: true,
|
|
||||||
tcpKeepAliveInitialDelay: 30000,
|
|
||||||
|
|
||||||
env: {
|
|
||||||
TERM: "xterm-256color",
|
|
||||||
LANG: "en_US.UTF-8",
|
|
||||||
LC_ALL: "en_US.UTF-8",
|
|
||||||
LC_CTYPE: "en_US.UTF-8",
|
|
||||||
LC_MESSAGES: "en_US.UTF-8",
|
|
||||||
LC_MONETARY: "en_US.UTF-8",
|
|
||||||
LC_NUMERIC: "en_US.UTF-8",
|
|
||||||
LC_TIME: "en_US.UTF-8",
|
|
||||||
LC_COLLATE: "en_US.UTF-8",
|
|
||||||
COLORTERM: "truecolor",
|
|
||||||
},
|
|
||||||
|
|
||||||
algorithms: {
|
algorithms: {
|
||||||
kex: [
|
kex: [
|
||||||
@@ -688,6 +707,15 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
"aes256-cbc",
|
"aes256-cbc",
|
||||||
"3des-cbc",
|
"3des-cbc",
|
||||||
],
|
],
|
||||||
|
serverHostKey: [
|
||||||
|
"ssh-rsa",
|
||||||
|
"rsa-sha2-256",
|
||||||
|
"rsa-sha2-512",
|
||||||
|
"ecdsa-sha2-nistp256",
|
||||||
|
"ecdsa-sha2-nistp384",
|
||||||
|
"ecdsa-sha2-nistp521",
|
||||||
|
"ssh-ed25519",
|
||||||
|
],
|
||||||
hmac: [
|
hmac: [
|
||||||
"hmac-sha2-256-etm@openssh.com",
|
"hmac-sha2-256-etm@openssh.com",
|
||||||
"hmac-sha2-512-etm@openssh.com",
|
"hmac-sha2-512-etm@openssh.com",
|
||||||
@@ -726,13 +754,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
if (resolvedCredentials.keyPassword) {
|
if (resolvedCredentials.keyPassword) {
|
||||||
connectConfig.passphrase = resolvedCredentials.keyPassword;
|
connectConfig.passphrase = resolvedCredentials.keyPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
resolvedCredentials.keyType &&
|
|
||||||
resolvedCredentials.keyType !== "auto"
|
|
||||||
) {
|
|
||||||
connectConfig.privateKeyType = resolvedCredentials.keyType;
|
|
||||||
}
|
|
||||||
} catch (keyError) {
|
} catch (keyError) {
|
||||||
sshLogger.error("SSH key format error: " + keyError.message);
|
sshLogger.error("SSH key format error: " + keyError.message);
|
||||||
ws.send(
|
ws.send(
|
||||||
@@ -766,7 +787,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
sshConn.connect(connectConfig);
|
sshConn.connect(connectConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleResize(data: { cols: number; rows: number }) {
|
function handleResize(data: ResizeData) {
|
||||||
if (sshStream && sshStream.setWindow) {
|
if (sshStream && sshStream.setWindow) {
|
||||||
sshStream.setWindow(data.rows, data.cols, data.rows, data.cols);
|
sshStream.setWindow(data.rows, data.cols, data.rows, data.cols);
|
||||||
ws.send(
|
ws.send(
|
||||||
@@ -788,8 +809,11 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
if (sshStream) {
|
if (sshStream) {
|
||||||
try {
|
try {
|
||||||
sshStream.end();
|
sshStream.end();
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
sshLogger.error("Error closing stream: " + e.message);
|
sshLogger.error(
|
||||||
|
"Error closing stream: " +
|
||||||
|
(e instanceof Error ? e.message : "Unknown error"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
sshStream = null;
|
sshStream = null;
|
||||||
}
|
}
|
||||||
@@ -797,8 +821,11 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
if (sshConn) {
|
if (sshConn) {
|
||||||
try {
|
try {
|
||||||
sshConn.end();
|
sshConn.end();
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
sshLogger.error("Error closing connection: " + e.message);
|
sshLogger.error(
|
||||||
|
"Error closing connection: " +
|
||||||
|
(e instanceof Error ? e.message : "Unknown error"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
sshConn = null;
|
sshConn = null;
|
||||||
}
|
}
|
||||||
@@ -809,8 +836,11 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
if (sshConn && sshStream) {
|
if (sshConn && sshStream) {
|
||||||
try {
|
try {
|
||||||
sshStream.write("\x00");
|
sshStream.write("\x00");
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
sshLogger.error("SSH keepalive failed: " + e.message);
|
sshLogger.error(
|
||||||
|
"SSH keepalive failed: " +
|
||||||
|
(e instanceof Error ? e.message : "Unknown error"),
|
||||||
|
);
|
||||||
cleanupSSH();
|
cleanupSSH();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,18 @@ interface JWTPayload {
|
|||||||
exp?: number;
|
exp?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AuthenticatedRequest extends Request {
|
||||||
|
userId?: string;
|
||||||
|
pendingTOTP?: boolean;
|
||||||
|
dataKey?: Buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequestWithHeaders extends Request {
|
||||||
|
headers: Request["headers"] & {
|
||||||
|
"x-forwarded-proto"?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
class AuthManager {
|
class AuthManager {
|
||||||
private static instance: AuthManager;
|
private static instance: AuthManager;
|
||||||
private systemCrypto: SystemCrypto;
|
private systemCrypto: SystemCrypto;
|
||||||
@@ -163,7 +175,10 @@ class AuthManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getSecureCookieOptions(req: any, maxAge: number = 24 * 60 * 60 * 1000) {
|
getSecureCookieOptions(
|
||||||
|
req: RequestWithHeaders,
|
||||||
|
maxAge: number = 24 * 60 * 60 * 1000,
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
httpOnly: false,
|
httpOnly: false,
|
||||||
secure: req.secure || req.headers["x-forwarded-proto"] === "https",
|
secure: req.secure || req.headers["x-forwarded-proto"] === "https",
|
||||||
@@ -175,10 +190,11 @@ class AuthManager {
|
|||||||
|
|
||||||
createAuthMiddleware() {
|
createAuthMiddleware() {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
let token = req.cookies?.jwt;
|
const authReq = req as AuthenticatedRequest;
|
||||||
|
let token = authReq.cookies?.jwt;
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
const authHeader = req.headers["authorization"];
|
const authHeader = authReq.headers["authorization"];
|
||||||
if (authHeader?.startsWith("Bearer ")) {
|
if (authHeader?.startsWith("Bearer ")) {
|
||||||
token = authHeader.split(" ")[1];
|
token = authHeader.split(" ")[1];
|
||||||
}
|
}
|
||||||
@@ -194,15 +210,16 @@ class AuthManager {
|
|||||||
return res.status(401).json({ error: "Invalid token" });
|
return res.status(401).json({ error: "Invalid token" });
|
||||||
}
|
}
|
||||||
|
|
||||||
(req as any).userId = payload.userId;
|
authReq.userId = payload.userId;
|
||||||
(req as any).pendingTOTP = payload.pendingTOTP;
|
authReq.pendingTOTP = payload.pendingTOTP;
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
createDataAccessMiddleware() {
|
createDataAccessMiddleware() {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const userId = (req as any).userId;
|
const authReq = req as AuthenticatedRequest;
|
||||||
|
const userId = authReq.userId;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return res.status(401).json({ error: "Authentication required" });
|
return res.status(401).json({ error: "Authentication required" });
|
||||||
}
|
}
|
||||||
@@ -215,7 +232,7 @@ class AuthManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
(req as any).dataKey = dataKey;
|
authReq.dataKey = dataKey;
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -256,8 +273,9 @@ class AuthManager {
|
|||||||
return res.status(403).json({ error: "Admin access required" });
|
return res.status(403).json({ error: "Admin access required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
(req as any).userId = payload.userId;
|
const authReq = req as AuthenticatedRequest;
|
||||||
(req as any).pendingTOTP = payload.pendingTOTP;
|
authReq.userId = payload.userId;
|
||||||
|
authReq.pendingTOTP = payload.pendingTOTP;
|
||||||
next();
|
next();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
databaseLogger.error("Failed to verify admin privileges", error, {
|
databaseLogger.error("Failed to verify admin privileges", error, {
|
||||||
|
|||||||
@@ -3,6 +3,19 @@ import { LazyFieldEncryption } from "./lazy-field-encryption.js";
|
|||||||
import { UserCrypto } from "./user-crypto.js";
|
import { UserCrypto } from "./user-crypto.js";
|
||||||
import { databaseLogger } from "./logger.js";
|
import { databaseLogger } from "./logger.js";
|
||||||
|
|
||||||
|
interface DatabaseInstance {
|
||||||
|
prepare: (sql: string) => {
|
||||||
|
all: (param?: unknown) => DatabaseRecord[];
|
||||||
|
get: (param?: unknown) => DatabaseRecord;
|
||||||
|
run: (...params: unknown[]) => unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DatabaseRecord {
|
||||||
|
id: number | string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
class DataCrypto {
|
class DataCrypto {
|
||||||
private static userCrypto: UserCrypto;
|
private static userCrypto: UserCrypto;
|
||||||
|
|
||||||
@@ -10,13 +23,13 @@ class DataCrypto {
|
|||||||
this.userCrypto = UserCrypto.getInstance();
|
this.userCrypto = UserCrypto.getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
static encryptRecord(
|
static encryptRecord<T extends Record<string, unknown>>(
|
||||||
tableName: string,
|
tableName: string,
|
||||||
record: Record<string, unknown>,
|
record: T,
|
||||||
userId: string,
|
userId: string,
|
||||||
userDataKey: Buffer,
|
userDataKey: Buffer,
|
||||||
): any {
|
): T {
|
||||||
const encryptedRecord = { ...record };
|
const encryptedRecord: Record<string, unknown> = { ...record };
|
||||||
const recordId = record.id || "temp-" + Date.now();
|
const recordId = record.id || "temp-" + Date.now();
|
||||||
|
|
||||||
for (const [fieldName, value] of Object.entries(record)) {
|
for (const [fieldName, value] of Object.entries(record)) {
|
||||||
@@ -30,18 +43,18 @@ class DataCrypto {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return encryptedRecord;
|
return encryptedRecord as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
static decryptRecord(
|
static decryptRecord<T extends Record<string, unknown>>(
|
||||||
tableName: string,
|
tableName: string,
|
||||||
record: Record<string, unknown>,
|
record: T,
|
||||||
userId: string,
|
userId: string,
|
||||||
userDataKey: Buffer,
|
userDataKey: Buffer,
|
||||||
): any {
|
): T {
|
||||||
if (!record) return record;
|
if (!record) return record;
|
||||||
|
|
||||||
const decryptedRecord = { ...record };
|
const decryptedRecord: Record<string, unknown> = { ...record };
|
||||||
const recordId = record.id;
|
const recordId = record.id;
|
||||||
|
|
||||||
for (const [fieldName, value] of Object.entries(record)) {
|
for (const [fieldName, value] of Object.entries(record)) {
|
||||||
@@ -55,30 +68,25 @@ class DataCrypto {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return decryptedRecord;
|
return decryptedRecord as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
static decryptRecords(
|
static decryptRecords<T extends Record<string, unknown>>(
|
||||||
tableName: string,
|
tableName: string,
|
||||||
records: unknown[],
|
records: T[],
|
||||||
userId: string,
|
userId: string,
|
||||||
userDataKey: Buffer,
|
userDataKey: Buffer,
|
||||||
): unknown[] {
|
): T[] {
|
||||||
if (!Array.isArray(records)) return records;
|
if (!Array.isArray(records)) return records;
|
||||||
return records.map((record) =>
|
return records.map((record) =>
|
||||||
this.decryptRecord(
|
this.decryptRecord(tableName, record, userId, userDataKey),
|
||||||
tableName,
|
|
||||||
record as Record<string, unknown>,
|
|
||||||
userId,
|
|
||||||
userDataKey,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async migrateUserSensitiveFields(
|
static async migrateUserSensitiveFields(
|
||||||
userId: string,
|
userId: string,
|
||||||
userDataKey: Buffer,
|
userDataKey: Buffer,
|
||||||
db: any,
|
db: DatabaseInstance,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
migrated: boolean;
|
migrated: boolean;
|
||||||
migratedTables: string[];
|
migratedTables: string[];
|
||||||
@@ -102,7 +110,7 @@ class DataCrypto {
|
|||||||
|
|
||||||
const sshDataRecords = db
|
const sshDataRecords = db
|
||||||
.prepare("SELECT * FROM ssh_data WHERE user_id = ?")
|
.prepare("SELECT * FROM ssh_data WHERE user_id = ?")
|
||||||
.all(userId);
|
.all(userId) as DatabaseRecord[];
|
||||||
for (const record of sshDataRecords) {
|
for (const record of sshDataRecords) {
|
||||||
const sensitiveFields =
|
const sensitiveFields =
|
||||||
LazyFieldEncryption.getSensitiveFieldsForTable("ssh_data");
|
LazyFieldEncryption.getSensitiveFieldsForTable("ssh_data");
|
||||||
@@ -137,7 +145,7 @@ class DataCrypto {
|
|||||||
|
|
||||||
const sshCredentialsRecords = db
|
const sshCredentialsRecords = db
|
||||||
.prepare("SELECT * FROM ssh_credentials WHERE user_id = ?")
|
.prepare("SELECT * FROM ssh_credentials WHERE user_id = ?")
|
||||||
.all(userId);
|
.all(userId) as DatabaseRecord[];
|
||||||
for (const record of sshCredentialsRecords) {
|
for (const record of sshCredentialsRecords) {
|
||||||
const sensitiveFields =
|
const sensitiveFields =
|
||||||
LazyFieldEncryption.getSensitiveFieldsForTable("ssh_credentials");
|
LazyFieldEncryption.getSensitiveFieldsForTable("ssh_credentials");
|
||||||
@@ -174,7 +182,7 @@ class DataCrypto {
|
|||||||
|
|
||||||
const userRecord = db
|
const userRecord = db
|
||||||
.prepare("SELECT * FROM users WHERE id = ?")
|
.prepare("SELECT * FROM users WHERE id = ?")
|
||||||
.get(userId);
|
.get(userId) as DatabaseRecord | undefined;
|
||||||
if (userRecord) {
|
if (userRecord) {
|
||||||
const sensitiveFields =
|
const sensitiveFields =
|
||||||
LazyFieldEncryption.getSensitiveFieldsForTable("users");
|
LazyFieldEncryption.getSensitiveFieldsForTable("users");
|
||||||
@@ -225,7 +233,7 @@ class DataCrypto {
|
|||||||
static async reencryptUserDataAfterPasswordReset(
|
static async reencryptUserDataAfterPasswordReset(
|
||||||
userId: string,
|
userId: string,
|
||||||
newUserDataKey: Buffer,
|
newUserDataKey: Buffer,
|
||||||
db: any,
|
db: DatabaseInstance,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
reencryptedTables: string[];
|
reencryptedTables: string[];
|
||||||
@@ -267,17 +275,21 @@ class DataCrypto {
|
|||||||
try {
|
try {
|
||||||
const records = db
|
const records = db
|
||||||
.prepare(`SELECT * FROM ${table} WHERE user_id = ?`)
|
.prepare(`SELECT * FROM ${table} WHERE user_id = ?`)
|
||||||
.all(userId);
|
.all(userId) as DatabaseRecord[];
|
||||||
|
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
const recordId = record.id.toString();
|
const recordId = record.id.toString();
|
||||||
|
const updatedRecord: DatabaseRecord = { ...record };
|
||||||
let needsUpdate = false;
|
let needsUpdate = false;
|
||||||
const updatedRecord = { ...record };
|
|
||||||
|
|
||||||
for (const fieldName of fields) {
|
for (const fieldName of fields) {
|
||||||
const fieldValue = record[fieldName];
|
const fieldValue = record[fieldName];
|
||||||
|
|
||||||
if (fieldValue && fieldValue.trim() !== "") {
|
if (
|
||||||
|
fieldValue &&
|
||||||
|
typeof fieldValue === "string" &&
|
||||||
|
fieldValue.trim() !== ""
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const reencryptedValue = FieldCrypto.encryptField(
|
const reencryptedValue = FieldCrypto.encryptField(
|
||||||
fieldValue,
|
fieldValue,
|
||||||
@@ -389,29 +401,29 @@ class DataCrypto {
|
|||||||
return userDataKey;
|
return userDataKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
static encryptRecordForUser(
|
static encryptRecordForUser<T extends Record<string, unknown>>(
|
||||||
tableName: string,
|
tableName: string,
|
||||||
record: Record<string, unknown>,
|
record: T,
|
||||||
userId: string,
|
userId: string,
|
||||||
): any {
|
): T {
|
||||||
const userDataKey = this.validateUserAccess(userId);
|
const userDataKey = this.validateUserAccess(userId);
|
||||||
return this.encryptRecord(tableName, record, userId, userDataKey);
|
return this.encryptRecord(tableName, record, userId, userDataKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
static decryptRecordForUser(
|
static decryptRecordForUser<T extends Record<string, unknown>>(
|
||||||
tableName: string,
|
tableName: string,
|
||||||
record: Record<string, unknown>,
|
record: T,
|
||||||
userId: string,
|
userId: string,
|
||||||
): any {
|
): T {
|
||||||
const userDataKey = this.validateUserAccess(userId);
|
const userDataKey = this.validateUserAccess(userId);
|
||||||
return this.decryptRecord(tableName, record, userId, userDataKey);
|
return this.decryptRecord(tableName, record, userId, userDataKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
static decryptRecordsForUser(
|
static decryptRecordsForUser<T extends Record<string, unknown>>(
|
||||||
tableName: string,
|
tableName: string,
|
||||||
records: unknown[],
|
records: T[],
|
||||||
userId: string,
|
userId: string,
|
||||||
): unknown[] {
|
): T[] {
|
||||||
const userDataKey = this.validateUserAccess(userId);
|
const userDataKey = this.validateUserAccess(userId);
|
||||||
return this.decryptRecords(tableName, records, userId, userDataKey);
|
return this.decryptRecords(tableName, records, userId, userDataKey);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,35 @@ import {
|
|||||||
} from "@/ui/main-axios.ts";
|
} from "@/ui/main-axios.ts";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface RecentFileData {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
lastOpened?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PinnedFileData {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShortcutData {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DirectoryItemData {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
type: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SidebarItem {
|
export interface SidebarItem {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -88,7 +117,9 @@ export function FileManagerSidebar({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const recentData = await getRecentFiles(currentHost.id);
|
const recentData = await getRecentFiles(currentHost.id);
|
||||||
const recentItems = recentData.slice(0, 5).map((item: any) => ({
|
const recentItems = (recentData as RecentFileData[])
|
||||||
|
.slice(0, 5)
|
||||||
|
.map((item: RecentFileData) => ({
|
||||||
id: `recent-${item.id}`,
|
id: `recent-${item.id}`,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
path: item.path,
|
path: item.path,
|
||||||
@@ -98,21 +129,25 @@ export function FileManagerSidebar({
|
|||||||
setRecentItems(recentItems);
|
setRecentItems(recentItems);
|
||||||
|
|
||||||
const pinnedData = await getPinnedFiles(currentHost.id);
|
const pinnedData = await getPinnedFiles(currentHost.id);
|
||||||
const pinnedItems = pinnedData.map((item: any) => ({
|
const pinnedItems = (pinnedData as PinnedFileData[]).map(
|
||||||
|
(item: PinnedFileData) => ({
|
||||||
id: `pinned-${item.id}`,
|
id: `pinned-${item.id}`,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
path: item.path,
|
path: item.path,
|
||||||
type: "pinned" as const,
|
type: "pinned" as const,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
setPinnedItems(pinnedItems);
|
setPinnedItems(pinnedItems);
|
||||||
|
|
||||||
const shortcutData = await getFolderShortcuts(currentHost.id);
|
const shortcutData = await getFolderShortcuts(currentHost.id);
|
||||||
const shortcutItems = shortcutData.map((item: any) => ({
|
const shortcutItems = (shortcutData as ShortcutData[]).map(
|
||||||
|
(item: ShortcutData) => ({
|
||||||
id: `shortcut-${item.id}`,
|
id: `shortcut-${item.id}`,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
path: item.path,
|
path: item.path,
|
||||||
type: "shortcut" as const,
|
type: "shortcut" as const,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
setShortcuts(shortcutItems);
|
setShortcuts(shortcutItems);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load quick access data:", error);
|
console.error("Failed to load quick access data:", error);
|
||||||
@@ -230,12 +265,12 @@ export function FileManagerSidebar({
|
|||||||
try {
|
try {
|
||||||
const response = await listSSHFiles(sshSessionId, "/");
|
const response = await listSSHFiles(sshSessionId, "/");
|
||||||
|
|
||||||
const rootFiles = response.files || [];
|
const rootFiles = (response.files || []) as DirectoryItemData[];
|
||||||
const rootFolders = rootFiles.filter(
|
const rootFolders = rootFiles.filter(
|
||||||
(item: any) => item.type === "directory",
|
(item: DirectoryItemData) => item.type === "directory",
|
||||||
);
|
);
|
||||||
|
|
||||||
const rootTreeItems = rootFolders.map((folder: any) => ({
|
const rootTreeItems = rootFolders.map((folder: DirectoryItemData) => ({
|
||||||
id: `folder-${folder.name}`,
|
id: `folder-${folder.name}`,
|
||||||
name: folder.name,
|
name: folder.name,
|
||||||
path: folder.path,
|
path: folder.path,
|
||||||
@@ -298,12 +333,12 @@ export function FileManagerSidebar({
|
|||||||
try {
|
try {
|
||||||
const subResponse = await listSSHFiles(sshSessionId, folderPath);
|
const subResponse = await listSSHFiles(sshSessionId, folderPath);
|
||||||
|
|
||||||
const subFiles = subResponse.files || [];
|
const subFiles = (subResponse.files || []) as DirectoryItemData[];
|
||||||
const subFolders = subFiles.filter(
|
const subFolders = subFiles.filter(
|
||||||
(item: any) => item.type === "directory",
|
(item: DirectoryItemData) => item.type === "directory",
|
||||||
);
|
);
|
||||||
|
|
||||||
const subTreeItems = subFolders.map((folder: any) => ({
|
const subTreeItems = subFolders.map((folder: DirectoryItemData) => ({
|
||||||
id: `folder-${folder.path.replace(/\//g, "-")}`,
|
id: `folder-${folder.path.replace(/\//g, "-")}`,
|
||||||
name: folder.name,
|
name: folder.name,
|
||||||
path: folder.path,
|
path: folder.path,
|
||||||
|
|||||||
@@ -27,8 +27,28 @@ import {
|
|||||||
SystemWidget,
|
SystemWidget,
|
||||||
} from "./widgets";
|
} from "./widgets";
|
||||||
|
|
||||||
|
interface HostConfig {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
ip: string;
|
||||||
|
username: string;
|
||||||
|
folder?: string;
|
||||||
|
enableFileManager?: boolean;
|
||||||
|
tunnelConnections?: unknown[];
|
||||||
|
statsConfig?: string | StatsConfig;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TabData {
|
||||||
|
id: number;
|
||||||
|
type: string;
|
||||||
|
title?: string;
|
||||||
|
hostConfig?: HostConfig;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
interface ServerProps {
|
interface ServerProps {
|
||||||
hostConfig?: any;
|
hostConfig?: HostConfig;
|
||||||
title?: string;
|
title?: string;
|
||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
isTopbarOpen?: boolean;
|
isTopbarOpen?: boolean;
|
||||||
@@ -44,7 +64,10 @@ export function Server({
|
|||||||
}: ServerProps): React.ReactElement {
|
}: ServerProps): React.ReactElement {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { state: sidebarState } = useSidebar();
|
const { state: sidebarState } = useSidebar();
|
||||||
const { addTab, tabs } = useTabs() as any;
|
const { addTab, tabs } = useTabs() as {
|
||||||
|
addTab: (tab: { type: string; [key: string]: unknown }) => number;
|
||||||
|
tabs: TabData[];
|
||||||
|
};
|
||||||
const [serverStatus, setServerStatus] = React.useState<"online" | "offline">(
|
const [serverStatus, setServerStatus] = React.useState<"online" | "offline">(
|
||||||
"offline",
|
"offline",
|
||||||
);
|
);
|
||||||
@@ -163,13 +186,16 @@ export function Server({
|
|||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setServerStatus(res?.status === "online" ? "online" : "offline");
|
setServerStatus(res?.status === "online" ? "online" : "offline");
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
if (error?.response?.status === 503) {
|
const err = error as {
|
||||||
|
response?: { status?: number };
|
||||||
|
};
|
||||||
|
if (err?.response?.status === 503) {
|
||||||
setServerStatus("offline");
|
setServerStatus("offline");
|
||||||
} else if (error?.response?.status === 504) {
|
} else if (err?.response?.status === 504) {
|
||||||
setServerStatus("offline");
|
setServerStatus("offline");
|
||||||
} else if (error?.response?.status === 404) {
|
} else if (err?.response?.status === 404) {
|
||||||
setServerStatus("offline");
|
setServerStatus("offline");
|
||||||
} else {
|
} else {
|
||||||
setServerStatus("offline");
|
setServerStatus("offline");
|
||||||
@@ -193,14 +219,18 @@ export function Server({
|
|||||||
});
|
});
|
||||||
setShowStatsUI(true);
|
setShowStatsUI(true);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setMetrics(null);
|
setMetrics(null);
|
||||||
setShowStatsUI(false);
|
setShowStatsUI(false);
|
||||||
|
const err = error as {
|
||||||
|
code?: string;
|
||||||
|
response?: { status?: number; data?: { error?: string } };
|
||||||
|
};
|
||||||
if (
|
if (
|
||||||
error?.code === "TOTP_REQUIRED" ||
|
err?.code === "TOTP_REQUIRED" ||
|
||||||
(error?.response?.status === 403 &&
|
(err?.response?.status === 403 &&
|
||||||
error?.response?.data?.error === "TOTP_REQUIRED")
|
err?.response?.data?.error === "TOTP_REQUIRED")
|
||||||
) {
|
) {
|
||||||
toast.error(t("serverStats.totpUnavailable"));
|
toast.error(t("serverStats.totpUnavailable"));
|
||||||
} else {
|
} else {
|
||||||
@@ -236,7 +266,7 @@ export function Server({
|
|||||||
const isFileManagerAlreadyOpen = React.useMemo(() => {
|
const isFileManagerAlreadyOpen = React.useMemo(() => {
|
||||||
if (!currentHostConfig) return false;
|
if (!currentHostConfig) return false;
|
||||||
return tabs.some(
|
return tabs.some(
|
||||||
(tab: any) =>
|
(tab: TabData) =>
|
||||||
tab.type === "file_manager" &&
|
tab.type === "file_manager" &&
|
||||||
tab.hostConfig?.id === currentHostConfig.id,
|
tab.hostConfig?.id === currentHostConfig.id,
|
||||||
);
|
);
|
||||||
@@ -291,32 +321,37 @@ export function Server({
|
|||||||
);
|
);
|
||||||
setMetrics(data);
|
setMetrics(data);
|
||||||
setShowStatsUI(true);
|
setShowStatsUI(true);
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
|
const err = error as {
|
||||||
|
code?: string;
|
||||||
|
status?: number;
|
||||||
|
response?: { status?: number; data?: { error?: string } };
|
||||||
|
};
|
||||||
if (
|
if (
|
||||||
error?.code === "TOTP_REQUIRED" ||
|
err?.code === "TOTP_REQUIRED" ||
|
||||||
(error?.response?.status === 403 &&
|
(err?.response?.status === 403 &&
|
||||||
error?.response?.data?.error === "TOTP_REQUIRED")
|
err?.response?.data?.error === "TOTP_REQUIRED")
|
||||||
) {
|
) {
|
||||||
toast.error(t("serverStats.totpUnavailable"));
|
toast.error(t("serverStats.totpUnavailable"));
|
||||||
setMetrics(null);
|
setMetrics(null);
|
||||||
setShowStatsUI(false);
|
setShowStatsUI(false);
|
||||||
} else if (
|
} else if (
|
||||||
error?.response?.status === 503 ||
|
err?.response?.status === 503 ||
|
||||||
error?.status === 503
|
err?.status === 503
|
||||||
) {
|
) {
|
||||||
setServerStatus("offline");
|
setServerStatus("offline");
|
||||||
setMetrics(null);
|
setMetrics(null);
|
||||||
setShowStatsUI(false);
|
setShowStatsUI(false);
|
||||||
} else if (
|
} else if (
|
||||||
error?.response?.status === 504 ||
|
err?.response?.status === 504 ||
|
||||||
error?.status === 504
|
err?.status === 504
|
||||||
) {
|
) {
|
||||||
setServerStatus("offline");
|
setServerStatus("offline");
|
||||||
setMetrics(null);
|
setMetrics(null);
|
||||||
setShowStatsUI(false);
|
setShowStatsUI(false);
|
||||||
} else if (
|
} else if (
|
||||||
error?.response?.status === 404 ||
|
err?.response?.status === 404 ||
|
||||||
error?.status === 404
|
err?.status === 404
|
||||||
) {
|
) {
|
||||||
setServerStatus("offline");
|
setServerStatus("offline");
|
||||||
setMetrics(null);
|
setMetrics(null);
|
||||||
|
|||||||
Reference in New Issue
Block a user