SECURITY: Fix authentication and file manager display issues
- Add JWT authentication middleware to file manager and metrics APIs - Fix WebSocket authentication timing race conditions - Resolve file manager grid view display issue by eliminating request ID complexity - Fix FileViewer translation function undefined error - Simplify SSH authentication flow and remove duplicate connection attempts - Ensure consistent user authentication across all services 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import { sshCredentials } from "../database/db/schema.js";
|
|||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { fileLogger } from "../utils/logger.js";
|
import { fileLogger } 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";
|
||||||
|
|
||||||
// Executable file detection utility function
|
// Executable file detection utility function
|
||||||
function isExecutableFile(permissions: string, fileName: string): boolean {
|
function isExecutableFile(permissions: string, fileName: string): boolean {
|
||||||
@@ -62,6 +63,10 @@ app.use(express.json({ limit: "1gb" }));
|
|||||||
app.use(express.urlencoded({ limit: "1gb", extended: true }));
|
app.use(express.urlencoded({ limit: "1gb", extended: true }));
|
||||||
app.use(express.raw({ limit: "5gb", type: "application/octet-stream" }));
|
app.use(express.raw({ limit: "5gb", type: "application/octet-stream" }));
|
||||||
|
|
||||||
|
// Initialize AuthManager and add authentication middleware
|
||||||
|
const authManager = AuthManager.getInstance();
|
||||||
|
app.use(authManager.createAuthMiddleware());
|
||||||
|
|
||||||
interface SSHSession {
|
interface SSHSession {
|
||||||
client: SSHClient;
|
client: SSHClient;
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
@@ -108,9 +113,19 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
|||||||
keyPassword,
|
keyPassword,
|
||||||
authType,
|
authType,
|
||||||
credentialId,
|
credentialId,
|
||||||
userId,
|
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
|
// Use authenticated user ID from middleware
|
||||||
|
const userId = (req as any).userId;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
fileLogger.error("SSH connection rejected: no authenticated user", {
|
||||||
|
operation: "file_connect_auth",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
return res.status(401).json({ error: "Authentication required" });
|
||||||
|
}
|
||||||
|
|
||||||
if (!sessionId || !ip || !username || !port) {
|
if (!sessionId || !ip || !username || !port) {
|
||||||
fileLogger.warn("Missing SSH connection parameters for file manager", {
|
fileLogger.warn("Missing SSH connection parameters for file manager", {
|
||||||
operation: "file_connect",
|
operation: "file_connect",
|
||||||
@@ -2052,9 +2067,21 @@ app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const PORT = 8084;
|
const PORT = 8084;
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, async () => {
|
||||||
fileLogger.success("File Manager API server started", {
|
fileLogger.success("File Manager API server started", {
|
||||||
operation: "server_start",
|
operation: "server_start",
|
||||||
port: PORT,
|
port: PORT,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize AuthManager for JWT verification
|
||||||
|
try {
|
||||||
|
await authManager.initialize();
|
||||||
|
fileLogger.info("AuthManager initialized for file manager", {
|
||||||
|
operation: "auth_init",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
fileLogger.error("Failed to initialize AuthManager", err, {
|
||||||
|
operation: "auth_init_error",
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { sshData, sshCredentials } from "../database/db/schema.js";
|
|||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { statsLogger } from "../utils/logger.js";
|
import { statsLogger } 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";
|
||||||
|
|
||||||
interface PooledConnection {
|
interface PooledConnection {
|
||||||
client: Client;
|
client: Client;
|
||||||
@@ -228,6 +229,7 @@ class MetricsCache {
|
|||||||
const connectionPool = new SSHConnectionPool();
|
const connectionPool = new SSHConnectionPool();
|
||||||
const requestQueue = new RequestQueue();
|
const requestQueue = new RequestQueue();
|
||||||
const metricsCache = new MetricsCache();
|
const metricsCache = new MetricsCache();
|
||||||
|
const authManager = AuthManager.getInstance();
|
||||||
|
|
||||||
type HostStatus = "online" | "offline";
|
type HostStatus = "online" | "offline";
|
||||||
|
|
||||||
@@ -303,19 +305,23 @@ app.use((req, res, next) => {
|
|||||||
});
|
});
|
||||||
app.use(express.json({ limit: "1mb" }));
|
app.use(express.json({ limit: "1mb" }));
|
||||||
|
|
||||||
|
// Add authentication middleware - Linus principle: eliminate special cases
|
||||||
|
app.use(authManager.createAuthMiddleware());
|
||||||
|
|
||||||
const hostStatuses: Map<number, StatusEntry> = new Map();
|
const hostStatuses: Map<number, StatusEntry> = new Map();
|
||||||
|
|
||||||
async function fetchAllHosts(): Promise<SSHHostWithCredentials[]> {
|
async function fetchAllHosts(userId: string): Promise<SSHHostWithCredentials[]> {
|
||||||
try {
|
try {
|
||||||
const hosts = await SimpleDBOps.selectEncrypted(
|
const hosts = await SimpleDBOps.select(
|
||||||
getDb().select().from(sshData),
|
getDb().select().from(sshData).where(eq(sshData.userId, userId)),
|
||||||
"ssh_data",
|
"ssh_data",
|
||||||
|
userId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const hostsWithCredentials: SSHHostWithCredentials[] = [];
|
const hostsWithCredentials: SSHHostWithCredentials[] = [];
|
||||||
for (const host of hosts) {
|
for (const host of hosts) {
|
||||||
try {
|
try {
|
||||||
const hostWithCreds = await resolveHostCredentials(host);
|
const hostWithCreds = await resolveHostCredentials(host, userId);
|
||||||
if (hostWithCreds) {
|
if (hostWithCreds) {
|
||||||
hostsWithCredentials.push(hostWithCreds);
|
hostsWithCredentials.push(hostWithCreds);
|
||||||
}
|
}
|
||||||
@@ -335,11 +341,13 @@ async function fetchAllHosts(): Promise<SSHHostWithCredentials[]> {
|
|||||||
|
|
||||||
async function fetchHostById(
|
async function fetchHostById(
|
||||||
id: number,
|
id: number,
|
||||||
|
userId: string,
|
||||||
): Promise<SSHHostWithCredentials | undefined> {
|
): Promise<SSHHostWithCredentials | undefined> {
|
||||||
try {
|
try {
|
||||||
const hosts = await SimpleDBOps.selectEncrypted(
|
const hosts = await SimpleDBOps.select(
|
||||||
getDb().select().from(sshData).where(eq(sshData.id, id)),
|
getDb().select().from(sshData).where(and(eq(sshData.id, id), eq(sshData.userId, userId))),
|
||||||
"ssh_data",
|
"ssh_data",
|
||||||
|
userId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hosts.length === 0) {
|
if (hosts.length === 0) {
|
||||||
@@ -347,7 +355,7 @@ async function fetchHostById(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const host = hosts[0];
|
const host = hosts[0];
|
||||||
return await resolveHostCredentials(host);
|
return await resolveHostCredentials(host, userId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
statsLogger.error(`Failed to fetch host ${id}`, err);
|
statsLogger.error(`Failed to fetch host ${id}`, err);
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -356,6 +364,7 @@ async function fetchHostById(
|
|||||||
|
|
||||||
async function resolveHostCredentials(
|
async function resolveHostCredentials(
|
||||||
host: any,
|
host: any,
|
||||||
|
userId: string,
|
||||||
): Promise<SSHHostWithCredentials | undefined> {
|
): Promise<SSHHostWithCredentials | undefined> {
|
||||||
try {
|
try {
|
||||||
const baseHost: any = {
|
const baseHost: any = {
|
||||||
@@ -387,17 +396,18 @@ async function resolveHostCredentials(
|
|||||||
|
|
||||||
if (host.credentialId) {
|
if (host.credentialId) {
|
||||||
try {
|
try {
|
||||||
const credentials = await SimpleDBOps.selectEncrypted(
|
const credentials = await SimpleDBOps.select(
|
||||||
getDb()
|
getDb()
|
||||||
.select()
|
.select()
|
||||||
.from(sshCredentials)
|
.from(sshCredentials)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(sshCredentials.id, host.credentialId),
|
eq(sshCredentials.id, host.credentialId),
|
||||||
eq(sshCredentials.userId, host.userId),
|
eq(sshCredentials.userId, userId),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
"ssh_credentials",
|
"ssh_credentials",
|
||||||
|
userId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (credentials.length > 0) {
|
if (credentials.length > 0) {
|
||||||
@@ -809,11 +819,19 @@ function tcpPing(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pollStatusesOnce(): Promise<void> {
|
async function pollStatusesOnce(userId?: string): Promise<void> {
|
||||||
const hosts = await fetchAllHosts();
|
if (!userId) {
|
||||||
|
statsLogger.warn("Skipping status poll - no authenticated user", {
|
||||||
|
operation: "status_poll",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hosts = await fetchAllHosts(userId);
|
||||||
if (hosts.length === 0) {
|
if (hosts.length === 0) {
|
||||||
statsLogger.warn("No hosts retrieved for status polling", {
|
statsLogger.warn("No hosts retrieved for status polling", {
|
||||||
operation: "status_poll",
|
operation: "status_poll",
|
||||||
|
userId,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -845,8 +863,10 @@ async function pollStatusesOnce(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.get("/status", async (req, res) => {
|
app.get("/status", async (req, res) => {
|
||||||
|
const userId = (req as any).userId;
|
||||||
|
|
||||||
if (hostStatuses.size === 0) {
|
if (hostStatuses.size === 0) {
|
||||||
await pollStatusesOnce();
|
await pollStatusesOnce(userId);
|
||||||
}
|
}
|
||||||
const result: Record<number, StatusEntry> = {};
|
const result: Record<number, StatusEntry> = {};
|
||||||
for (const [id, entry] of hostStatuses.entries()) {
|
for (const [id, entry] of hostStatuses.entries()) {
|
||||||
@@ -857,9 +877,10 @@ app.get("/status", async (req, res) => {
|
|||||||
|
|
||||||
app.get("/status/:id", validateHostId, async (req, res) => {
|
app.get("/status/:id", validateHostId, async (req, res) => {
|
||||||
const id = Number(req.params.id);
|
const id = Number(req.params.id);
|
||||||
|
const userId = (req as any).userId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const host = await fetchHostById(id);
|
const host = await fetchHostById(id, userId);
|
||||||
if (!host) {
|
if (!host) {
|
||||||
return res.status(404).json({ error: "Host not found" });
|
return res.status(404).json({ error: "Host not found" });
|
||||||
}
|
}
|
||||||
@@ -880,15 +901,17 @@ app.get("/status/:id", validateHostId, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post("/refresh", async (req, res) => {
|
app.post("/refresh", async (req, res) => {
|
||||||
await pollStatusesOnce();
|
const userId = (req as any).userId;
|
||||||
|
await pollStatusesOnce(userId);
|
||||||
res.json({ message: "Refreshed" });
|
res.json({ message: "Refreshed" });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/metrics/:id", validateHostId, async (req, res) => {
|
app.get("/metrics/:id", validateHostId, async (req, res) => {
|
||||||
const id = Number(req.params.id);
|
const id = Number(req.params.id);
|
||||||
|
const userId = (req as any).userId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const host = await fetchHostById(id);
|
const host = await fetchHostById(id, userId);
|
||||||
if (!host) {
|
if (!host) {
|
||||||
return res.status(404).json({ error: "Host not found" });
|
return res.status(404).json({ error: "Host not found" });
|
||||||
}
|
}
|
||||||
@@ -947,11 +970,21 @@ app.listen(PORT, async () => {
|
|||||||
operation: "server_start",
|
operation: "server_start",
|
||||||
port: PORT,
|
port: PORT,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize AuthManager for JWT verification
|
||||||
try {
|
try {
|
||||||
await pollStatusesOnce();
|
await authManager.initialize();
|
||||||
|
statsLogger.info("AuthManager initialized for metrics collection", {
|
||||||
|
operation: "auth_init",
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
statsLogger.error("Initial poll failed", err, {
|
statsLogger.error("Failed to initialize AuthManager", err, {
|
||||||
operation: "initial_poll",
|
operation: "auth_init_error",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip initial poll - requires user authentication
|
||||||
|
statsLogger.info("Server ready - status polling will begin with first authenticated request", {
|
||||||
|
operation: "server_ready",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,24 +24,50 @@ const wss = new WebSocketServer({
|
|||||||
const url = parseUrl(info.req.url!, true);
|
const url = parseUrl(info.req.url!, true);
|
||||||
const token = url.query.token as string;
|
const token = url.query.token as string;
|
||||||
|
|
||||||
|
// DEBUG: Log detailed JWT verification process
|
||||||
|
sshLogger.debug("WebSocket JWT verification starting", {
|
||||||
|
operation: "websocket_jwt_debug",
|
||||||
|
fullUrl: info.req.url,
|
||||||
|
hasToken: !!token,
|
||||||
|
tokenLength: token?.length || 0,
|
||||||
|
tokenStart: token ? token.substring(0, 20) + "..." : "missing",
|
||||||
|
ip: info.req.socket.remoteAddress
|
||||||
|
});
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
sshLogger.warn("WebSocket connection rejected: missing token", {
|
sshLogger.warn("WebSocket connection rejected: missing token", {
|
||||||
operation: "websocket_auth_reject",
|
operation: "websocket_auth_reject",
|
||||||
reason: "missing_token",
|
reason: "missing_token",
|
||||||
origin: info.origin,
|
origin: info.origin,
|
||||||
ip: info.req.socket.remoteAddress
|
ip: info.req.socket.remoteAddress,
|
||||||
|
queryKeys: Object.keys(url.query || {})
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify JWT token
|
// Verify JWT token
|
||||||
|
sshLogger.debug("Calling authManager.verifyJWTToken", {
|
||||||
|
operation: "websocket_jwt_verify",
|
||||||
|
tokenLength: token.length
|
||||||
|
});
|
||||||
|
|
||||||
const payload = await authManager.verifyJWTToken(token);
|
const payload = await authManager.verifyJWTToken(token);
|
||||||
|
|
||||||
|
sshLogger.debug("JWT verification result", {
|
||||||
|
operation: "websocket_jwt_result",
|
||||||
|
hasPayload: !!payload,
|
||||||
|
payloadKeys: payload ? Object.keys(payload) : [],
|
||||||
|
userId: payload?.userId || "none"
|
||||||
|
});
|
||||||
|
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
sshLogger.warn("WebSocket connection rejected: invalid token", {
|
sshLogger.warn("WebSocket connection rejected: invalid token", {
|
||||||
operation: "websocket_auth_reject",
|
operation: "websocket_auth_reject",
|
||||||
reason: "invalid_token",
|
reason: "invalid_token",
|
||||||
origin: info.origin,
|
origin: info.origin,
|
||||||
ip: info.req.socket.remoteAddress
|
ip: info.req.socket.remoteAddress,
|
||||||
|
tokenLength: token.length,
|
||||||
|
tokenStart: token.substring(0, 20) + "..."
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -70,9 +96,8 @@ const wss = new WebSocketServer({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach user info to request object
|
// Note: We don't need to attach user info to request anymore
|
||||||
(info.req as any).userId = payload.userId;
|
// Connection handler will re-verify JWT directly from URL
|
||||||
(info.req as any).userPayload = payload;
|
|
||||||
|
|
||||||
sshLogger.info("WebSocket connection authenticated", {
|
sshLogger.info("WebSocket connection authenticated", {
|
||||||
operation: "websocket_auth_success",
|
operation: "websocket_auth_success",
|
||||||
@@ -97,14 +122,42 @@ sshLogger.success("SSH Terminal WebSocket server started with authentication", {
|
|||||||
features: ["JWT_auth", "connection_limits", "data_access_control"]
|
features: ["JWT_auth", "connection_limits", "data_access_control"]
|
||||||
});
|
});
|
||||||
|
|
||||||
wss.on("connection", (ws: WebSocket, req) => {
|
wss.on("connection", async (ws: WebSocket, req) => {
|
||||||
// Extract authenticated user info from request
|
// Linus principle: eliminate complexity - always parse JWT from URL directly
|
||||||
const userId = (req as any).userId;
|
let userId: string | undefined;
|
||||||
const userPayload = (req as any).userPayload;
|
let userPayload: any;
|
||||||
|
|
||||||
if (!userId) {
|
try {
|
||||||
sshLogger.error("WebSocket connection without authentication - should not happen", {
|
const url = parseUrl(req.url!, true);
|
||||||
operation: "websocket_security_violation",
|
const token = url.query.token as string;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
sshLogger.warn("WebSocket connection rejected: missing token in connection", {
|
||||||
|
operation: "websocket_connection_reject",
|
||||||
|
reason: "missing_token",
|
||||||
|
ip: req.socket.remoteAddress
|
||||||
|
});
|
||||||
|
ws.close(1008, "Authentication required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = await authManager.verifyJWTToken(token);
|
||||||
|
if (!payload) {
|
||||||
|
sshLogger.warn("WebSocket connection rejected: invalid token in connection", {
|
||||||
|
operation: "websocket_connection_reject",
|
||||||
|
reason: "invalid_token",
|
||||||
|
ip: req.socket.remoteAddress
|
||||||
|
});
|
||||||
|
ws.close(1008, "Authentication required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
userId = payload.userId;
|
||||||
|
userPayload = payload;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
sshLogger.error("WebSocket JWT verification failed during connection", error, {
|
||||||
|
operation: "websocket_connection_auth_error",
|
||||||
ip: req.socket.remoteAddress
|
ip: req.socket.remoteAddress
|
||||||
});
|
});
|
||||||
ws.close(1008, "Authentication required");
|
ws.close(1008, "Authentication required");
|
||||||
|
|||||||
@@ -1138,24 +1138,6 @@ export function FileManagerGrid({
|
|||||||
(f) => f.path === file.path,
|
(f) => f.path === file.path,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Detailed debug path comparison
|
|
||||||
if (selectedFiles.length > 0) {
|
|
||||||
console.log(`\n=== File: ${file.name} ===`);
|
|
||||||
console.log(`File path: "${file.path}"`);
|
|
||||||
console.log(
|
|
||||||
`Selected files:`,
|
|
||||||
selectedFiles.map((f) => `"${f.path}"`),
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`Path comparison results:`,
|
|
||||||
selectedFiles.map(
|
|
||||||
(f) =>
|
|
||||||
`"${f.path}" === "${file.path}" -> ${f.path === file.path}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
console.log(`Final isSelected: ${isSelected}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={file.path}
|
key={file.path}
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
|||||||
const [files, setFiles] = useState<FileItem[]>([]);
|
const [files, setFiles] = useState<FileItem[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [sshSessionId, setSshSessionId] = useState<string | null>(null);
|
const [sshSessionId, setSshSessionId] = useState<string | null>(null);
|
||||||
const [currentRequestId, setCurrentRequestId] = useState<number>(0);
|
|
||||||
const [isReconnecting, setIsReconnecting] = useState<boolean>(false);
|
const [isReconnecting, setIsReconnecting] = useState<boolean>(false);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [lastRefreshTime, setLastRefreshTime] = useState<number>(0);
|
const [lastRefreshTime, setLastRefreshTime] = useState<number>(0);
|
||||||
@@ -231,56 +230,39 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadDirectory(path: string) {
|
const loadDirectory = useCallback(async (path: string) => {
|
||||||
if (!sshSessionId) {
|
if (!sshSessionId) {
|
||||||
console.error("Cannot load directory: no SSH session ID");
|
console.error("Cannot load directory: no SSH session ID");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate unique request ID to prevent race conditions
|
|
||||||
const requestId = Date.now();
|
|
||||||
setCurrentRequestId(requestId);
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[${requestId}] Loading directory:`, path);
|
console.log("Loading directory:", path);
|
||||||
|
|
||||||
const response = await listSSHFiles(sshSessionId, path);
|
const response = await listSSHFiles(sshSessionId, path);
|
||||||
|
|
||||||
// Only process response if this is still the latest request
|
console.log("Directory response received:", response);
|
||||||
if (requestId !== currentRequestId) {
|
|
||||||
console.log(`[${requestId}] Request outdated, ignoring response`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[${requestId}] Directory response received:`, response);
|
|
||||||
|
|
||||||
const files = Array.isArray(response) ? response : response?.files || [];
|
const files = Array.isArray(response) ? response : response?.files || [];
|
||||||
|
|
||||||
|
console.log("Directory loaded successfully:", files.length, "items");
|
||||||
|
|
||||||
setFiles(files);
|
setFiles(files);
|
||||||
clearSelection();
|
clearSelection();
|
||||||
|
|
||||||
console.log(`[${requestId}] Directory loaded successfully:`, files.length, "items");
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Only handle error if this is still the latest request
|
console.error("Failed to load directory:", error);
|
||||||
if (requestId !== currentRequestId) {
|
|
||||||
console.log(`[${requestId}] Request outdated, ignoring error`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error(`[${requestId}] Failed to load directory:`, error);
|
|
||||||
toast.error(
|
toast.error(
|
||||||
t("fileManager.failedToLoadDirectory") + ": " + (error.message || error)
|
t("fileManager.failedToLoadDirectory") + ": " + (error.message || error)
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
// Only clear loading if this is still the latest request
|
|
||||||
if (requestId === currentRequestId) {
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}, [sshSessionId, clearSelection, t]);
|
||||||
}
|
|
||||||
|
|
||||||
// Debounced refresh function - prevent excessive clicking
|
// Debounced refresh function - prevent excessive clicking
|
||||||
function handleRefreshDirectory() {
|
const handleRefreshDirectory = useCallback(() => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const DEBOUNCE_MS = 500; // 500ms debounce
|
const DEBOUNCE_MS = 500; // 500ms debounce
|
||||||
|
|
||||||
@@ -291,7 +273,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
|||||||
|
|
||||||
setLastRefreshTime(now);
|
setLastRefreshTime(now);
|
||||||
loadDirectory(currentPath);
|
loadDirectory(currentPath);
|
||||||
}
|
}, [currentPath, lastRefreshTime, loadDirectory]);
|
||||||
|
|
||||||
function handleFilesDropped(fileList: FileList) {
|
function handleFilesDropped(fileList: FileList) {
|
||||||
if (!sshSessionId) {
|
if (!sshSessionId) {
|
||||||
@@ -1465,6 +1447,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
|||||||
file.name.toLowerCase().includes(searchQuery.toLowerCase()),
|
file.name.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
if (!currentHost) {
|
if (!currentHost) {
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex items-center justify-center">
|
<div className="h-full flex items-center justify-center">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
FileText,
|
FileText,
|
||||||
Image as ImageIcon,
|
Image as ImageIcon,
|
||||||
@@ -276,6 +277,7 @@ export function FileViewer({
|
|||||||
onSave,
|
onSave,
|
||||||
onDownload,
|
onDownload,
|
||||||
}: FileViewerProps) {
|
}: FileViewerProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [editedContent, setEditedContent] = useState(content);
|
const [editedContent, setEditedContent] = useState(content);
|
||||||
const [originalContent, setOriginalContent] = useState(
|
const [originalContent, setOriginalContent] = useState(
|
||||||
savedContent || content,
|
savedContent || content,
|
||||||
|
|||||||
Reference in New Issue
Block a user