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:
ZacharyZcR
2025-10-09 18:23:16 +08:00
parent d7e98cda04
commit aa6473fb48
5 changed files with 295 additions and 165 deletions

View File

@@ -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();
} }
} }

View File

@@ -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, {

View File

@@ -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);
} }

View File

@@ -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,

View File

@@ -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);