fix: resolve TypeScript and ESLint errors across the codebase
- Fixed @typescript-eslint/no-unused-vars errors (31 instances) - Fixed @typescript-eslint/no-explicit-any errors in backend (~22 instances) - Fixed @typescript-eslint/no-explicit-any errors in frontend (~60 instances) - Fixed prefer-const errors (5 instances) - Fixed no-empty-object-type and rules-of-hooks errors - Added proper type assertions for database operations - Improved type safety in authentication and encryption modules - Enhanced type definitions for API routes and SSH operations All TypeScript compilation errors resolved. Application builds and runs successfully.
This commit is contained in:
@@ -702,24 +702,22 @@ router.get("/oidc/callback", async (req, res) => {
|
|||||||
const getNestedValue = (
|
const getNestedValue = (
|
||||||
obj: Record<string, unknown>,
|
obj: Record<string, unknown>,
|
||||||
path: string,
|
path: string,
|
||||||
): any => {
|
): unknown => {
|
||||||
if (!path || !obj) return null;
|
if (!path || !obj) return null;
|
||||||
return path.split(".").reduce((current, key) => current?.[key], obj);
|
return path.split(".").reduce((current, key) => current?.[key], obj);
|
||||||
};
|
};
|
||||||
|
|
||||||
const identifier =
|
const identifier = (getNestedValue(userInfo, config.identifier_path) ||
|
||||||
getNestedValue(userInfo, config.identifier_path) ||
|
|
||||||
userInfo[config.identifier_path] ||
|
userInfo[config.identifier_path] ||
|
||||||
userInfo.sub ||
|
userInfo.sub ||
|
||||||
userInfo.email ||
|
userInfo.email ||
|
||||||
userInfo.preferred_username;
|
userInfo.preferred_username) as string;
|
||||||
|
|
||||||
const name =
|
const name = (getNestedValue(userInfo, config.name_path) ||
|
||||||
getNestedValue(userInfo, config.name_path) ||
|
|
||||||
userInfo[config.name_path] ||
|
userInfo[config.name_path] ||
|
||||||
userInfo.name ||
|
userInfo.name ||
|
||||||
userInfo.given_name ||
|
userInfo.given_name ||
|
||||||
identifier;
|
identifier) as string;
|
||||||
|
|
||||||
if (!identifier) {
|
if (!identifier) {
|
||||||
authLogger.error(
|
authLogger.error(
|
||||||
@@ -753,14 +751,14 @@ router.get("/oidc/callback", async (req, res) => {
|
|||||||
is_admin: isFirstUser,
|
is_admin: isFirstUser,
|
||||||
is_oidc: true,
|
is_oidc: true,
|
||||||
oidc_identifier: identifier,
|
oidc_identifier: identifier,
|
||||||
client_id: config.client_id,
|
client_id: String(config.client_id),
|
||||||
client_secret: config.client_secret,
|
client_secret: String(config.client_secret),
|
||||||
issuer_url: config.issuer_url,
|
issuer_url: String(config.issuer_url),
|
||||||
authorization_url: config.authorization_url,
|
authorization_url: String(config.authorization_url),
|
||||||
token_url: config.token_url,
|
token_url: String(config.token_url),
|
||||||
identifier_path: config.identifier_path,
|
identifier_path: String(config.identifier_path),
|
||||||
name_path: config.name_path,
|
name_path: String(config.name_path),
|
||||||
scopes: config.scopes,
|
scopes: String(config.scopes),
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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";
|
import { AuthManager } from "../utils/auth-manager.js";
|
||||||
|
import type { AuthenticatedRequest } from "../../types/index.js";
|
||||||
|
|
||||||
function isExecutableFile(permissions: string, fileName: string): boolean {
|
function isExecutableFile(permissions: string, fileName: string): boolean {
|
||||||
const hasExecutePermission =
|
const hasExecutePermission =
|
||||||
@@ -166,7 +167,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
|||||||
credentialId,
|
credentialId,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
const userId = (req as any).userId;
|
const userId = (req as AuthenticatedRequest).userId;
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
fileLogger.error("SSH connection rejected: no authenticated user", {
|
fileLogger.error("SSH connection rejected: no authenticated user", {
|
||||||
@@ -246,7 +247,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const config: any = {
|
const config: Record<string, unknown> = {
|
||||||
host: ip,
|
host: ip,
|
||||||
port: port || 22,
|
port: port || 22,
|
||||||
username,
|
username,
|
||||||
@@ -417,7 +418,9 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (resolvedCredentials.password) {
|
if (resolvedCredentials.password) {
|
||||||
const responses = prompts.map(() => resolvedCredentials.password || "");
|
const responses = prompts.map(
|
||||||
|
() => resolvedCredentials.password || "",
|
||||||
|
);
|
||||||
finish(responses);
|
finish(responses);
|
||||||
} else {
|
} else {
|
||||||
finish(prompts.map(() => ""));
|
finish(prompts.map(() => ""));
|
||||||
@@ -432,7 +435,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
|||||||
app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
|
app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
|
||||||
const { sessionId, totpCode } = req.body;
|
const { sessionId, totpCode } = req.body;
|
||||||
|
|
||||||
const userId = (req as any).userId;
|
const userId = (req as AuthenticatedRequest).userId;
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
fileLogger.error("TOTP verification rejected: no authenticated user", {
|
fileLogger.error("TOTP verification rejected: no authenticated user", {
|
||||||
@@ -454,7 +457,9 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
|
|||||||
sessionId,
|
sessionId,
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
return res.status(404).json({ error: "TOTP session expired. Please reconnect." });
|
return res
|
||||||
|
.status(404)
|
||||||
|
.json({ error: "TOTP session expired. Please reconnect." });
|
||||||
}
|
}
|
||||||
|
|
||||||
delete pendingTOTPSessions[sessionId];
|
delete pendingTOTPSessions[sessionId];
|
||||||
@@ -462,8 +467,12 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
|
|||||||
if (Date.now() - session.createdAt > 120000) {
|
if (Date.now() - session.createdAt > 120000) {
|
||||||
try {
|
try {
|
||||||
session.client.end();
|
session.client.end();
|
||||||
} catch {}
|
} catch {
|
||||||
return res.status(408).json({ error: "TOTP session timeout. Please reconnect." });
|
// Ignore errors when closing timed out session
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
.status(408)
|
||||||
|
.json({ error: "TOTP session timeout. Please reconnect." });
|
||||||
}
|
}
|
||||||
|
|
||||||
session.finish([totpCode]);
|
session.finish([totpCode]);
|
||||||
@@ -487,7 +496,10 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
|
|||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({ status: "success", message: "TOTP verified, SSH connection established" });
|
res.json({
|
||||||
|
status: "success",
|
||||||
|
message: "TOTP verified, SSH connection established",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
session.client.on("error", (err) => {
|
session.client.on("error", (err) => {
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ class SSHConnectionPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class RequestQueue {
|
class RequestQueue {
|
||||||
private queues = new Map<number, Array<() => Promise<any>>>();
|
private queues = new Map<number, Array<() => Promise<unknown>>>();
|
||||||
private processing = new Set<number>();
|
private processing = new Set<number>();
|
||||||
|
|
||||||
async queueRequest<T>(hostId: number, request: () => Promise<T>): Promise<T> {
|
async queueRequest<T>(hostId: number, request: () => Promise<T>): Promise<T> {
|
||||||
|
|||||||
@@ -903,7 +903,7 @@ async function connectSSHTunnel(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const connOptions: any = {
|
const connOptions: Record<string, unknown> = {
|
||||||
host: tunnelConfig.sourceIP,
|
host: tunnelConfig.sourceIP,
|
||||||
port: tunnelConfig.sourceSSHPort,
|
port: tunnelConfig.sourceSSHPort,
|
||||||
username: tunnelConfig.sourceUsername,
|
username: tunnelConfig.sourceUsername,
|
||||||
@@ -1065,7 +1065,7 @@ async function killRemoteTunnelByMarker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const conn = new Client();
|
const conn = new Client();
|
||||||
const connOptions: any = {
|
const connOptions: Record<string, unknown> = {
|
||||||
host: tunnelConfig.sourceIP,
|
host: tunnelConfig.sourceIP,
|
||||||
port: tunnelConfig.sourceSSHPort,
|
port: tunnelConfig.sourceSSHPort,
|
||||||
username: tunnelConfig.sourceUsername,
|
username: tunnelConfig.sourceUsername,
|
||||||
@@ -1461,10 +1461,10 @@ async function initializeAutoStartTunnels(): Promise<void> {
|
|||||||
});
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
tunnelLogger.error(
|
tunnelLogger.error(
|
||||||
"Failed to initialize auto-start tunnels:",
|
"Failed to initialize auto-start tunnels:",
|
||||||
error.message,
|
error instanceof Error ? error.message : "Unknown error",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,11 @@ class DatabaseFileEncryption {
|
|||||||
|
|
||||||
const iv = crypto.randomBytes(16);
|
const iv = crypto.randomBytes(16);
|
||||||
|
|
||||||
const cipher = crypto.createCipheriv(this.ALGORITHM, key, iv) as any;
|
const cipher = crypto.createCipheriv(
|
||||||
|
this.ALGORITHM,
|
||||||
|
key,
|
||||||
|
iv,
|
||||||
|
) as crypto.CipherGCM;
|
||||||
const encrypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
|
const encrypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
|
||||||
const tag = cipher.getAuthTag();
|
const tag = cipher.getAuthTag();
|
||||||
|
|
||||||
@@ -78,7 +82,11 @@ class DatabaseFileEncryption {
|
|||||||
|
|
||||||
const iv = crypto.randomBytes(16);
|
const iv = crypto.randomBytes(16);
|
||||||
|
|
||||||
const cipher = crypto.createCipheriv(this.ALGORITHM, key, iv) as any;
|
const cipher = crypto.createCipheriv(
|
||||||
|
this.ALGORITHM,
|
||||||
|
key,
|
||||||
|
iv,
|
||||||
|
) as crypto.CipherGCM;
|
||||||
const encrypted = Buffer.concat([
|
const encrypted = Buffer.concat([
|
||||||
cipher.update(sourceData),
|
cipher.update(sourceData),
|
||||||
cipher.final(),
|
cipher.final(),
|
||||||
@@ -163,7 +171,7 @@ class DatabaseFileEncryption {
|
|||||||
metadata.algorithm,
|
metadata.algorithm,
|
||||||
key,
|
key,
|
||||||
Buffer.from(metadata.iv, "hex"),
|
Buffer.from(metadata.iv, "hex"),
|
||||||
) as any;
|
) as crypto.DecipherGCM;
|
||||||
decipher.setAuthTag(Buffer.from(metadata.tag, "hex"));
|
decipher.setAuthTag(Buffer.from(metadata.tag, "hex"));
|
||||||
|
|
||||||
const decryptedBuffer = Buffer.concat([
|
const decryptedBuffer = Buffer.concat([
|
||||||
@@ -233,7 +241,7 @@ class DatabaseFileEncryption {
|
|||||||
metadata.algorithm,
|
metadata.algorithm,
|
||||||
key,
|
key,
|
||||||
Buffer.from(metadata.iv, "hex"),
|
Buffer.from(metadata.iv, "hex"),
|
||||||
) as any;
|
) as crypto.DecipherGCM;
|
||||||
decipher.setAuthTag(Buffer.from(metadata.tag, "hex"));
|
decipher.setAuthTag(Buffer.from(metadata.tag, "hex"));
|
||||||
|
|
||||||
const decrypted = Buffer.concat([
|
const decrypted = Buffer.concat([
|
||||||
|
|||||||
@@ -234,7 +234,9 @@ export class DatabaseMigration {
|
|||||||
memoryDb.exec("PRAGMA foreign_keys = OFF");
|
memoryDb.exec("PRAGMA foreign_keys = OFF");
|
||||||
|
|
||||||
for (const table of tables) {
|
for (const table of tables) {
|
||||||
const rows = originalDb.prepare(`SELECT * FROM ${table.name}`).all();
|
const rows = originalDb
|
||||||
|
.prepare(`SELECT * FROM ${table.name}`)
|
||||||
|
.all() as Record<string, unknown>[];
|
||||||
|
|
||||||
if (rows.length > 0) {
|
if (rows.length > 0) {
|
||||||
const columns = Object.keys(rows[0]);
|
const columns = Object.keys(rows[0]);
|
||||||
@@ -244,7 +246,7 @@ export class DatabaseMigration {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const insertTransaction = memoryDb.transaction(
|
const insertTransaction = memoryDb.transaction(
|
||||||
(dataRows: any[]) => {
|
(dataRows: Record<string, unknown>[]) => {
|
||||||
for (const row of dataRows) {
|
for (const row of dataRows) {
|
||||||
const values = columns.map((col) => row[col]);
|
const values = columns.map((col) => row[col]);
|
||||||
insertStmt.run(values);
|
insertStmt.run(values);
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { FieldCrypto } from "./field-crypto.js";
|
import { FieldCrypto } from "./field-crypto.js";
|
||||||
import { databaseLogger } from "./logger.js";
|
import { databaseLogger } from "./logger.js";
|
||||||
|
|
||||||
|
interface DatabaseInstance {
|
||||||
|
prepare: (sql: string) => {
|
||||||
|
all: (param?: unknown) => unknown[];
|
||||||
|
get: (param?: unknown) => unknown;
|
||||||
|
run: (...params: unknown[]) => unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export class LazyFieldEncryption {
|
export class LazyFieldEncryption {
|
||||||
private static readonly LEGACY_FIELD_NAME_MAP: Record<string, string> = {
|
private static readonly LEGACY_FIELD_NAME_MAP: Record<string, string> = {
|
||||||
key_password: "keyPassword",
|
key_password: "keyPassword",
|
||||||
@@ -182,12 +190,12 @@ export class LazyFieldEncryption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static migrateRecordSensitiveFields(
|
static migrateRecordSensitiveFields(
|
||||||
record: any,
|
record: Record<string, unknown>,
|
||||||
sensitiveFields: string[],
|
sensitiveFields: string[],
|
||||||
userKEK: Buffer,
|
userKEK: Buffer,
|
||||||
recordId: string,
|
recordId: string,
|
||||||
): {
|
): {
|
||||||
updatedRecord: any;
|
updatedRecord: Record<string, unknown>;
|
||||||
migratedFields: string[];
|
migratedFields: string[];
|
||||||
needsUpdate: boolean;
|
needsUpdate: boolean;
|
||||||
} {
|
} {
|
||||||
@@ -202,7 +210,7 @@ export class LazyFieldEncryption {
|
|||||||
try {
|
try {
|
||||||
const { encrypted, wasPlaintext, wasLegacyEncryption } =
|
const { encrypted, wasPlaintext, wasLegacyEncryption } =
|
||||||
this.migrateFieldToEncrypted(
|
this.migrateFieldToEncrypted(
|
||||||
fieldValue,
|
fieldValue as string,
|
||||||
userKEK,
|
userKEK,
|
||||||
recordId,
|
recordId,
|
||||||
fieldName,
|
fieldName,
|
||||||
@@ -279,7 +287,7 @@ export class LazyFieldEncryption {
|
|||||||
static async checkUserNeedsMigration(
|
static async checkUserNeedsMigration(
|
||||||
userId: string,
|
userId: string,
|
||||||
userKEK: Buffer,
|
userKEK: Buffer,
|
||||||
db: any,
|
db: DatabaseInstance,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
needsMigration: boolean;
|
needsMigration: boolean;
|
||||||
plaintextFields: Array<{
|
plaintextFields: Array<{
|
||||||
@@ -298,7 +306,9 @@ export class LazyFieldEncryption {
|
|||||||
try {
|
try {
|
||||||
const sshHosts = db
|
const sshHosts = db
|
||||||
.prepare("SELECT * FROM ssh_data WHERE user_id = ?")
|
.prepare("SELECT * FROM ssh_data WHERE user_id = ?")
|
||||||
.all(userId);
|
.all(userId) as Array<
|
||||||
|
Record<string, unknown> & { id: string | number }
|
||||||
|
>;
|
||||||
for (const host of sshHosts) {
|
for (const host of sshHosts) {
|
||||||
const sensitiveFields = this.getSensitiveFieldsForTable("ssh_data");
|
const sensitiveFields = this.getSensitiveFieldsForTable("ssh_data");
|
||||||
const hostPlaintextFields: string[] = [];
|
const hostPlaintextFields: string[] = [];
|
||||||
@@ -307,7 +317,7 @@ export class LazyFieldEncryption {
|
|||||||
if (
|
if (
|
||||||
host[field] &&
|
host[field] &&
|
||||||
this.fieldNeedsMigration(
|
this.fieldNeedsMigration(
|
||||||
host[field],
|
host[field] as string,
|
||||||
userKEK,
|
userKEK,
|
||||||
host.id.toString(),
|
host.id.toString(),
|
||||||
field,
|
field,
|
||||||
@@ -329,7 +339,9 @@ export class LazyFieldEncryption {
|
|||||||
|
|
||||||
const sshCredentials = db
|
const sshCredentials = db
|
||||||
.prepare("SELECT * FROM ssh_credentials WHERE user_id = ?")
|
.prepare("SELECT * FROM ssh_credentials WHERE user_id = ?")
|
||||||
.all(userId);
|
.all(userId) as Array<
|
||||||
|
Record<string, unknown> & { id: string | number }
|
||||||
|
>;
|
||||||
for (const credential of sshCredentials) {
|
for (const credential of sshCredentials) {
|
||||||
const sensitiveFields =
|
const sensitiveFields =
|
||||||
this.getSensitiveFieldsForTable("ssh_credentials");
|
this.getSensitiveFieldsForTable("ssh_credentials");
|
||||||
@@ -339,7 +351,7 @@ export class LazyFieldEncryption {
|
|||||||
if (
|
if (
|
||||||
credential[field] &&
|
credential[field] &&
|
||||||
this.fieldNeedsMigration(
|
this.fieldNeedsMigration(
|
||||||
credential[field],
|
credential[field] as string,
|
||||||
userKEK,
|
userKEK,
|
||||||
credential.id.toString(),
|
credential.id.toString(),
|
||||||
field,
|
field,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export interface LogContext {
|
|||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
[key: string]: any;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SENSITIVE_FIELDS = [
|
const SENSITIVE_FIELDS = [
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class UserDataImport {
|
|||||||
) {
|
) {
|
||||||
const importStats = await this.importSshHosts(
|
const importStats = await this.importSshHosts(
|
||||||
targetUserId,
|
targetUserId,
|
||||||
exportData.userData.sshHosts,
|
exportData.userData.sshHosts as Record<string, unknown>[],
|
||||||
{ replaceExisting, dryRun, userDataKey },
|
{ replaceExisting, dryRun, userDataKey },
|
||||||
);
|
);
|
||||||
result.summary.sshHostsImported = importStats.imported;
|
result.summary.sshHostsImported = importStats.imported;
|
||||||
@@ -104,7 +104,7 @@ class UserDataImport {
|
|||||||
) {
|
) {
|
||||||
const importStats = await this.importSshCredentials(
|
const importStats = await this.importSshCredentials(
|
||||||
targetUserId,
|
targetUserId,
|
||||||
exportData.userData.sshCredentials,
|
exportData.userData.sshCredentials as Record<string, unknown>[],
|
||||||
{ replaceExisting, dryRun, userDataKey },
|
{ replaceExisting, dryRun, userDataKey },
|
||||||
);
|
);
|
||||||
result.summary.sshCredentialsImported = importStats.imported;
|
result.summary.sshCredentialsImported = importStats.imported;
|
||||||
@@ -129,7 +129,7 @@ class UserDataImport {
|
|||||||
) {
|
) {
|
||||||
const importStats = await this.importDismissedAlerts(
|
const importStats = await this.importDismissedAlerts(
|
||||||
targetUserId,
|
targetUserId,
|
||||||
exportData.userData.dismissedAlerts,
|
exportData.userData.dismissedAlerts as Record<string, unknown>[],
|
||||||
{ replaceExisting, dryRun },
|
{ replaceExisting, dryRun },
|
||||||
);
|
);
|
||||||
result.summary.dismissedAlertsImported = importStats.imported;
|
result.summary.dismissedAlertsImported = importStats.imported;
|
||||||
@@ -159,7 +159,7 @@ class UserDataImport {
|
|||||||
|
|
||||||
private static async importSshHosts(
|
private static async importSshHosts(
|
||||||
targetUserId: string,
|
targetUserId: string,
|
||||||
sshHosts: any[],
|
sshHosts: Record<string, unknown>[],
|
||||||
options: {
|
options: {
|
||||||
replaceExisting: boolean;
|
replaceExisting: boolean;
|
||||||
dryRun: boolean;
|
dryRun: boolean;
|
||||||
@@ -198,7 +198,9 @@ class UserDataImport {
|
|||||||
|
|
||||||
delete processedHostData.id;
|
delete processedHostData.id;
|
||||||
|
|
||||||
await getDb().insert(sshData).values(processedHostData);
|
await getDb()
|
||||||
|
.insert(sshData)
|
||||||
|
.values(processedHostData as unknown as typeof sshData.$inferInsert);
|
||||||
imported++;
|
imported++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errors.push(
|
errors.push(
|
||||||
@@ -213,7 +215,7 @@ class UserDataImport {
|
|||||||
|
|
||||||
private static async importSshCredentials(
|
private static async importSshCredentials(
|
||||||
targetUserId: string,
|
targetUserId: string,
|
||||||
credentials: any[],
|
credentials: Record<string, unknown>[],
|
||||||
options: {
|
options: {
|
||||||
replaceExisting: boolean;
|
replaceExisting: boolean;
|
||||||
dryRun: boolean;
|
dryRun: boolean;
|
||||||
@@ -254,7 +256,11 @@ class UserDataImport {
|
|||||||
|
|
||||||
delete processedCredentialData.id;
|
delete processedCredentialData.id;
|
||||||
|
|
||||||
await getDb().insert(sshCredentials).values(processedCredentialData);
|
await getDb()
|
||||||
|
.insert(sshCredentials)
|
||||||
|
.values(
|
||||||
|
processedCredentialData as unknown as typeof sshCredentials.$inferInsert,
|
||||||
|
);
|
||||||
imported++;
|
imported++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errors.push(
|
errors.push(
|
||||||
@@ -269,7 +275,7 @@ class UserDataImport {
|
|||||||
|
|
||||||
private static async importFileManagerData(
|
private static async importFileManagerData(
|
||||||
targetUserId: string,
|
targetUserId: string,
|
||||||
fileManagerData: any,
|
fileManagerData: Record<string, unknown>,
|
||||||
options: { replaceExisting: boolean; dryRun: boolean },
|
options: { replaceExisting: boolean; dryRun: boolean },
|
||||||
) {
|
) {
|
||||||
let imported = 0;
|
let imported = 0;
|
||||||
@@ -356,7 +362,7 @@ class UserDataImport {
|
|||||||
|
|
||||||
private static async importDismissedAlerts(
|
private static async importDismissedAlerts(
|
||||||
targetUserId: string,
|
targetUserId: string,
|
||||||
alerts: any[],
|
alerts: Record<string, unknown>[],
|
||||||
options: { replaceExisting: boolean; dryRun: boolean },
|
options: { replaceExisting: boolean; dryRun: boolean },
|
||||||
) {
|
) {
|
||||||
let imported = 0;
|
let imported = 0;
|
||||||
@@ -376,7 +382,7 @@ class UserDataImport {
|
|||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(dismissedAlerts.userId, targetUserId),
|
eq(dismissedAlerts.userId, targetUserId),
|
||||||
eq(dismissedAlerts.alertId, alert.alertId),
|
eq(dismissedAlerts.alertId, alert.alertId as string),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -395,10 +401,12 @@ class UserDataImport {
|
|||||||
if (existing.length > 0 && options.replaceExisting) {
|
if (existing.length > 0 && options.replaceExisting) {
|
||||||
await getDb()
|
await getDb()
|
||||||
.update(dismissedAlerts)
|
.update(dismissedAlerts)
|
||||||
.set(newAlert)
|
.set(newAlert as typeof dismissedAlerts.$inferInsert)
|
||||||
.where(eq(dismissedAlerts.id, existing[0].id));
|
.where(eq(dismissedAlerts.id, existing[0].id));
|
||||||
} else {
|
} else {
|
||||||
await getDb().insert(dismissedAlerts).values(newAlert);
|
await getDb()
|
||||||
|
.insert(dismissedAlerts)
|
||||||
|
.values(newAlert as typeof dismissedAlerts.$inferInsert);
|
||||||
}
|
}
|
||||||
|
|
||||||
imported++;
|
imported++;
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import { Eye, EyeOff } from "lucide-react";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface PasswordInputProps
|
type PasswordInputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
||||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
||||||
|
|
||||||
export const PasswordInput = React.forwardRef<
|
export const PasswordInput = React.forwardRef<
|
||||||
HTMLInputElement,
|
HTMLInputElement,
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|||||||
|
|
||||||
const originalToast = toast;
|
const originalToast = toast;
|
||||||
|
|
||||||
const rateLimitedToast = (message: string, options?: any) => {
|
const rateLimitedToast = (
|
||||||
|
message: string,
|
||||||
|
options?: Record<string, unknown>,
|
||||||
|
) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const lastToast = lastToastRef.current;
|
const lastToast = lastToastRef.current;
|
||||||
|
|
||||||
@@ -25,13 +28,13 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(toast, {
|
Object.assign(toast, {
|
||||||
success: (message: string, options?: any) =>
|
success: (message: string, options?: Record<string, unknown>) =>
|
||||||
rateLimitedToast(message, { ...options, type: "success" }),
|
rateLimitedToast(message, { ...options, type: "success" }),
|
||||||
error: (message: string, options?: any) =>
|
error: (message: string, options?: Record<string, unknown>) =>
|
||||||
rateLimitedToast(message, { ...options, type: "error" }),
|
rateLimitedToast(message, { ...options, type: "error" }),
|
||||||
warning: (message: string, options?: any) =>
|
warning: (message: string, options?: Record<string, unknown>) =>
|
||||||
rateLimitedToast(message, { ...options, type: "warning" }),
|
rateLimitedToast(message, { ...options, type: "warning" }),
|
||||||
info: (message: string, options?: any) =>
|
info: (message: string, options?: Record<string, unknown>) =>
|
||||||
rateLimitedToast(message, { ...options, type: "info" }),
|
rateLimitedToast(message, { ...options, type: "info" }),
|
||||||
message: rateLimitedToast,
|
message: rateLimitedToast,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import * as React from "react";
|
|||||||
|
|
||||||
import { cn } from "../../lib/utils";
|
import { cn } from "../../lib/utils";
|
||||||
|
|
||||||
export interface TextareaProps
|
export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
|
||||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
|
||||||
|
|
||||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||||
({ className, ...props }, ref) => {
|
({ className, ...props }, ref) => {
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ export function VersionCheckModal({
|
|||||||
isAuthenticated = false,
|
isAuthenticated = false,
|
||||||
}: VersionCheckModalProps) {
|
}: VersionCheckModalProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [versionInfo, setVersionInfo] = useState<any>(null);
|
const [versionInfo, setVersionInfo] = useState<Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
> | null>(null);
|
||||||
const [versionChecking, setVersionChecking] = useState(false);
|
const [versionChecking, setVersionChecking] = useState(false);
|
||||||
const [versionDismissed] = useState(false);
|
const [versionDismissed] = useState(false);
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export interface LogContext {
|
|||||||
errorCode?: string;
|
errorCode?: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
|
|
||||||
[key: string]: any;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
class FrontendLogger {
|
class FrontendLogger {
|
||||||
|
|||||||
@@ -155,7 +155,9 @@ export function CredentialEditor({
|
|||||||
type FormData = z.infer<typeof formSchema>;
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
const form = useForm<FormData>({
|
const form = useForm<FormData>({
|
||||||
resolver: zodResolver(formSchema) as any,
|
resolver: zodResolver(formSchema) as unknown as Parameters<
|
||||||
|
typeof useForm<FormData>
|
||||||
|
>[0]["resolver"],
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
@@ -198,7 +200,7 @@ export function CredentialEditor({
|
|||||||
formData.publicKey = fullCredentialDetails.publicKey || "";
|
formData.publicKey = fullCredentialDetails.publicKey || "";
|
||||||
formData.keyPassword = fullCredentialDetails.keyPassword || "";
|
formData.keyPassword = fullCredentialDetails.keyPassword || "";
|
||||||
formData.keyType =
|
formData.keyType =
|
||||||
(fullCredentialDetails.keyType as any) || ("auto" as const);
|
(fullCredentialDetails.keyType as string) || ("auto" as const);
|
||||||
}
|
}
|
||||||
|
|
||||||
form.reset(formData);
|
form.reset(formData);
|
||||||
|
|||||||
@@ -71,7 +71,15 @@ export function CredentialsManager({
|
|||||||
const [showDeployDialog, setShowDeployDialog] = useState(false);
|
const [showDeployDialog, setShowDeployDialog] = useState(false);
|
||||||
const [deployingCredential, setDeployingCredential] =
|
const [deployingCredential, setDeployingCredential] =
|
||||||
useState<Credential | null>(null);
|
useState<Credential | null>(null);
|
||||||
const [availableHosts, setAvailableHosts] = useState<any[]>([]);
|
const [availableHosts, setAvailableHosts] = useState<
|
||||||
|
Array<{
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
ip: string;
|
||||||
|
port: number;
|
||||||
|
username: string;
|
||||||
|
}>
|
||||||
|
>([]);
|
||||||
const [selectedHostId, setSelectedHostId] = useState<string>("");
|
const [selectedHostId, setSelectedHostId] = useState<string>("");
|
||||||
const [deployLoading, setDeployLoading] = useState(false);
|
const [deployLoading, setDeployLoading] = useState(false);
|
||||||
const [hostSearchQuery, setHostSearchQuery] = useState("");
|
const [hostSearchQuery, setHostSearchQuery] = useState("");
|
||||||
@@ -207,10 +215,13 @@ export function CredentialsManager({
|
|||||||
);
|
);
|
||||||
await fetchCredentials();
|
await fetchCredentials();
|
||||||
window.dispatchEvent(new CustomEvent("credentials:changed"));
|
window.dispatchEvent(new CustomEvent("credentials:changed"));
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
if (err.response?.data?.details) {
|
const error = err as {
|
||||||
|
response?: { data?: { error?: string; details?: string } };
|
||||||
|
};
|
||||||
|
if (error.response?.data?.details) {
|
||||||
toast.error(
|
toast.error(
|
||||||
`${err.response.data.error}\n${err.response.data.details}`,
|
`${error.response.data.error}\n${error.response.data.details}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.error(t("credentials.failedToDeleteCredential"));
|
toast.error(t("credentials.failedToDeleteCredential"));
|
||||||
|
|||||||
@@ -96,15 +96,19 @@ export function DiffViewer({
|
|||||||
|
|
||||||
setContent1(response1.content || "");
|
setContent1(response1.content || "");
|
||||||
setContent2(response2.content || "");
|
setContent2(response2.content || "");
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error("Failed to load files for diff:", error);
|
console.error("Failed to load files for diff:", error);
|
||||||
|
|
||||||
const errorData = error?.response?.data;
|
const err = error as {
|
||||||
|
message?: string;
|
||||||
|
response?: { data?: { tooLarge?: boolean; error?: string } };
|
||||||
|
};
|
||||||
|
const errorData = err?.response?.data;
|
||||||
if (errorData?.tooLarge) {
|
if (errorData?.tooLarge) {
|
||||||
setError(t("fileManager.fileTooLarge", { error: errorData.error }));
|
setError(t("fileManager.fileTooLarge", { error: errorData.error }));
|
||||||
} else if (
|
} else if (
|
||||||
error.message?.includes("connection") ||
|
err.message?.includes("connection") ||
|
||||||
error.message?.includes("established")
|
err.message?.includes("established")
|
||||||
) {
|
) {
|
||||||
setError(
|
setError(
|
||||||
t("fileManager.sshConnectionFailed", {
|
t("fileManager.sshConnectionFailed", {
|
||||||
@@ -117,9 +121,7 @@ export function DiffViewer({
|
|||||||
setError(
|
setError(
|
||||||
t("fileManager.loadFileFailed", {
|
t("fileManager.loadFileFailed", {
|
||||||
error:
|
error:
|
||||||
error.message ||
|
err.message || errorData?.error || t("fileManager.unknownError"),
|
||||||
errorData?.error ||
|
|
||||||
t("fileManager.unknownError"),
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -157,12 +159,13 @@ export function DiffViewer({
|
|||||||
t("fileManager.downloadFileSuccess", { name: file.name }),
|
t("fileManager.downloadFileSuccess", { name: file.name }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error("Failed to download file:", error);
|
console.error("Failed to download file:", error);
|
||||||
|
const err = error as { message?: string };
|
||||||
toast.error(
|
toast.error(
|
||||||
t("fileManager.downloadFileFailed") +
|
t("fileManager.downloadFileFailed") +
|
||||||
": " +
|
": " +
|
||||||
(error.message || t("fileManager.unknownError")),
|
(err.message || t("fileManager.unknownError")),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ function getLanguageExtension(filename: string) {
|
|||||||
return language ? loadLanguage(language) : null;
|
return language ? loadLanguage(language) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatFileSize(bytes?: number, t?: any): string {
|
function formatFileSize(bytes?: number, t?: (key: string) => string): string {
|
||||||
if (!bytes) return t ? t("fileManager.unknownSize") : "Unknown size";
|
if (!bytes) return t ? t("fileManager.unknownSize") : "Unknown size";
|
||||||
const sizes = ["B", "KB", "MB", "GB"];
|
const sizes = ["B", "KB", "MB", "GB"];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
@@ -323,7 +323,9 @@ export function FileViewer({
|
|||||||
const [pdfScale, setPdfScale] = useState(1.2);
|
const [pdfScale, setPdfScale] = useState(1.2);
|
||||||
const [pdfError, setPdfError] = useState(false);
|
const [pdfError, setPdfError] = useState(false);
|
||||||
const [markdownEditMode, setMarkdownEditMode] = useState(false);
|
const [markdownEditMode, setMarkdownEditMode] = useState(false);
|
||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<{
|
||||||
|
view?: { dispatch: (transaction: unknown) => void };
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
const fileTypeInfo = getFileType(file.name);
|
const fileTypeInfo = getFileType(file.name);
|
||||||
|
|
||||||
|
|||||||
@@ -157,28 +157,40 @@ export function FileWindow({
|
|||||||
|
|
||||||
const extension = file.name.split(".").pop()?.toLowerCase();
|
const extension = file.name.split(".").pop()?.toLowerCase();
|
||||||
setIsEditable(!mediaExtensions.includes(extension || ""));
|
setIsEditable(!mediaExtensions.includes(extension || ""));
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error("Failed to load file:", error);
|
console.error("Failed to load file:", error);
|
||||||
|
|
||||||
const errorData = error?.response?.data;
|
const err = error as {
|
||||||
|
message?: string;
|
||||||
|
isFileNotFound?: boolean;
|
||||||
|
response?: {
|
||||||
|
status?: number;
|
||||||
|
data?: {
|
||||||
|
tooLarge?: boolean;
|
||||||
|
error?: string;
|
||||||
|
fileNotFound?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const errorData = err?.response?.data;
|
||||||
if (errorData?.tooLarge) {
|
if (errorData?.tooLarge) {
|
||||||
toast.error(`File too large: ${errorData.error}`, {
|
toast.error(`File too large: ${errorData.error}`, {
|
||||||
duration: 10000,
|
duration: 10000,
|
||||||
});
|
});
|
||||||
} else if (
|
} else if (
|
||||||
error.message?.includes("connection") ||
|
err.message?.includes("connection") ||
|
||||||
error.message?.includes("established")
|
err.message?.includes("established")
|
||||||
) {
|
) {
|
||||||
toast.error(
|
toast.error(
|
||||||
`SSH connection failed. Please check your connection to ${sshHost.name} (${sshHost.ip}:${sshHost.port})`,
|
`SSH connection failed. Please check your connection to ${sshHost.name} (${sshHost.ip}:${sshHost.port})`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
errorData?.error || error.message || "Unknown error";
|
errorData?.error || err.message || "Unknown error";
|
||||||
const isFileNotFound =
|
const isFileNotFound =
|
||||||
(error as any).isFileNotFound ||
|
err.isFileNotFound ||
|
||||||
errorData?.fileNotFound ||
|
errorData?.fileNotFound ||
|
||||||
error.response?.status === 404 ||
|
err.response?.status === 404 ||
|
||||||
errorMessage.includes("File not found") ||
|
errorMessage.includes("File not found") ||
|
||||||
errorMessage.includes("No such file or directory") ||
|
errorMessage.includes("No such file or directory") ||
|
||||||
errorMessage.includes("cannot access") ||
|
errorMessage.includes("cannot access") ||
|
||||||
@@ -229,10 +241,11 @@ export function FileWindow({
|
|||||||
const contentSize = new Blob([fileContent]).size;
|
const contentSize = new Blob([fileContent]).size;
|
||||||
file.size = contentSize;
|
file.size = contentSize;
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error("Failed to load file content:", error);
|
console.error("Failed to load file content:", error);
|
||||||
|
const err = error as { message?: string };
|
||||||
toast.error(
|
toast.error(
|
||||||
`${t("fileManager.failedToLoadFile")}: ${error.message || t("fileManager.unknownError")}`,
|
`${t("fileManager.failedToLoadFile")}: ${err.message || t("fileManager.unknownError")}`,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -258,19 +271,20 @@ export function FileWindow({
|
|||||||
}
|
}
|
||||||
|
|
||||||
toast.success(t("fileManager.fileSavedSuccessfully"));
|
toast.success(t("fileManager.fileSavedSuccessfully"));
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error("Failed to save file:", error);
|
console.error("Failed to save file:", error);
|
||||||
|
|
||||||
|
const err = error as { message?: string };
|
||||||
if (
|
if (
|
||||||
error.message?.includes("connection") ||
|
err.message?.includes("connection") ||
|
||||||
error.message?.includes("established")
|
err.message?.includes("established")
|
||||||
) {
|
) {
|
||||||
toast.error(
|
toast.error(
|
||||||
`SSH connection failed. Please check your connection to ${sshHost.name} (${sshHost.ip}:${sshHost.port})`,
|
`SSH connection failed. Please check your connection to ${sshHost.name} (${sshHost.ip}:${sshHost.port})`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.error(
|
||||||
`${t("fileManager.failedToSaveFile")}: ${error.message || t("fileManager.unknownError")}`,
|
`${t("fileManager.failedToSaveFile")}: ${err.message || t("fileManager.unknownError")}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -335,19 +349,20 @@ export function FileWindow({
|
|||||||
|
|
||||||
toast.success(t("fileManager.fileDownloadedSuccessfully"));
|
toast.success(t("fileManager.fileDownloadedSuccessfully"));
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error("Failed to download file:", error);
|
console.error("Failed to download file:", error);
|
||||||
|
|
||||||
|
const err = error as { message?: string };
|
||||||
if (
|
if (
|
||||||
error.message?.includes("connection") ||
|
err.message?.includes("connection") ||
|
||||||
error.message?.includes("established")
|
err.message?.includes("established")
|
||||||
) {
|
) {
|
||||||
toast.error(
|
toast.error(
|
||||||
`SSH connection failed. Please check your connection to ${sshHost.name} (${sshHost.ip}:${sshHost.port})`,
|
`SSH connection failed. Please check your connection to ${sshHost.name} (${sshHost.ip}:${sshHost.port})`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.error(
|
||||||
`Failed to download file: ${error.message || "Unknown error"}`,
|
`Failed to download file: ${err.message || "Unknown error"}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,9 +38,17 @@ export function TerminalWindow({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { closeWindow, maximizeWindow, focusWindow, windows } =
|
const { closeWindow, maximizeWindow, focusWindow, windows } =
|
||||||
useWindowManager();
|
useWindowManager();
|
||||||
const terminalRef = React.useRef<any>(null);
|
const terminalRef = React.useRef<{ fit?: () => void } | null>(null);
|
||||||
const resizeTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);
|
const resizeTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (resizeTimeoutRef.current) {
|
||||||
|
clearTimeout(resizeTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const currentWindow = windows.find((w) => w.id === windowId);
|
const currentWindow = windows.find((w) => w.id === windowId);
|
||||||
if (!currentWindow) {
|
if (!currentWindow) {
|
||||||
return null;
|
return null;
|
||||||
@@ -70,14 +78,6 @@ export function TerminalWindow({
|
|||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (resizeTimeoutRef.current) {
|
|
||||||
clearTimeout(resizeTimeoutRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const terminalTitle = executeCommand
|
const terminalTitle = executeCommand
|
||||||
? t("terminal.runTitle", { host: hostConfig.name, command: executeCommand })
|
? t("terminal.runTitle", { host: hostConfig.name, command: executeCommand })
|
||||||
: initialPath
|
: initialPath
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ export function HostManager({
|
|||||||
const [activeTab, setActiveTab] = useState("host_viewer");
|
const [activeTab, setActiveTab] = useState("host_viewer");
|
||||||
const [editingHost, setEditingHost] = useState<SSHHost | null>(null);
|
const [editingHost, setEditingHost] = useState<SSHHost | null>(null);
|
||||||
|
|
||||||
const [editingCredential, setEditingCredential] = useState<any | null>(null);
|
const [editingCredential, setEditingCredential] = useState<{
|
||||||
|
id: number;
|
||||||
|
name?: string;
|
||||||
|
username: string;
|
||||||
|
} | null>(null);
|
||||||
const { state: sidebarState } = useSidebar();
|
const { state: sidebarState } = useSidebar();
|
||||||
|
|
||||||
const handleEditHost = (host: SSHHost) => {
|
const handleEditHost = (host: SSHHost) => {
|
||||||
@@ -34,7 +38,11 @@ export function HostManager({
|
|||||||
setActiveTab("host_viewer");
|
setActiveTab("host_viewer");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditCredential = (credential: any) => {
|
const handleEditCredential = (credential: {
|
||||||
|
id: number;
|
||||||
|
name?: string;
|
||||||
|
username: string;
|
||||||
|
}) => {
|
||||||
setEditingCredential(credential);
|
setEditingCredential(credential);
|
||||||
setActiveTab("add_credential");
|
setActiveTab("add_credential");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -60,7 +60,14 @@ interface SSHHost {
|
|||||||
enableTunnel: boolean;
|
enableTunnel: boolean;
|
||||||
enableFileManager: boolean;
|
enableFileManager: boolean;
|
||||||
defaultPath: string;
|
defaultPath: string;
|
||||||
tunnelConnections: any[];
|
tunnelConnections: Array<{
|
||||||
|
sourcePort: number;
|
||||||
|
endpointPort: number;
|
||||||
|
endpointHost: string;
|
||||||
|
maxRetries: number;
|
||||||
|
retryInterval: number;
|
||||||
|
autoStart: boolean;
|
||||||
|
}>;
|
||||||
statsConfig?: StatsConfig;
|
statsConfig?: StatsConfig;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -79,7 +86,9 @@ export function HostManagerEditor({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [folders, setFolders] = useState<string[]>([]);
|
const [folders, setFolders] = useState<string[]>([]);
|
||||||
const [sshConfigurations, setSshConfigurations] = useState<string[]>([]);
|
const [sshConfigurations, setSshConfigurations] = useState<string[]>([]);
|
||||||
const [credentials, setCredentials] = useState<any[]>([]);
|
const [credentials, setCredentials] = useState<
|
||||||
|
Array<{ id: number; username: string; authType: string }>
|
||||||
|
>([]);
|
||||||
|
|
||||||
const [authTab, setAuthTab] = useState<"password" | "key" | "credential">(
|
const [authTab, setAuthTab] = useState<"password" | "key" | "credential">(
|
||||||
"password",
|
"password",
|
||||||
@@ -292,7 +301,7 @@ export function HostManagerEditor({
|
|||||||
type FormData = z.infer<typeof formSchema>;
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
const form = useForm<FormData>({
|
const form = useForm<FormData>({
|
||||||
resolver: zodResolver(formSchema) as any,
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
ip: "",
|
ip: "",
|
||||||
@@ -377,7 +386,17 @@ export function HostManagerEditor({
|
|||||||
} else if (defaultAuthType === "key") {
|
} else if (defaultAuthType === "key") {
|
||||||
formData.key = editingHost.id ? "existing_key" : editingHost.key;
|
formData.key = editingHost.id ? "existing_key" : editingHost.key;
|
||||||
formData.keyPassword = cleanedHost.keyPassword || "";
|
formData.keyPassword = cleanedHost.keyPassword || "";
|
||||||
formData.keyType = (cleanedHost.keyType as any) || "auto";
|
formData.keyType =
|
||||||
|
(cleanedHost.keyType as
|
||||||
|
| "auto"
|
||||||
|
| "ssh-rsa"
|
||||||
|
| "ssh-ed25519"
|
||||||
|
| "ecdsa-sha2-nistp256"
|
||||||
|
| "ecdsa-sha2-nistp384"
|
||||||
|
| "ecdsa-sha2-nistp521"
|
||||||
|
| "ssh-dss"
|
||||||
|
| "ssh-rsa-sha2-256"
|
||||||
|
| "ssh-rsa-sha2-512") || "auto";
|
||||||
} else if (defaultAuthType === "credential") {
|
} else if (defaultAuthType === "credential") {
|
||||||
formData.credentialId =
|
formData.credentialId =
|
||||||
cleanedHost.credentialId || "existing_credential";
|
cleanedHost.credentialId || "existing_credential";
|
||||||
@@ -430,7 +449,7 @@ export function HostManagerEditor({
|
|||||||
data.name = `${data.username}@${data.ip}`;
|
data.name = `${data.username}@${data.ip}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitData: any = {
|
const submitData: Record<string, unknown> = {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
ip: data.ip,
|
ip: data.ip,
|
||||||
port: data.port,
|
port: data.port,
|
||||||
|
|||||||
@@ -11,7 +11,16 @@ interface NetworkWidgetProps {
|
|||||||
export function NetworkWidget({ metrics }: NetworkWidgetProps) {
|
export function NetworkWidget({ metrics }: NetworkWidgetProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const network = (metrics as any)?.network;
|
const metricsWithNetwork = metrics as ServerMetrics & {
|
||||||
|
network?: {
|
||||||
|
interfaces?: Array<{
|
||||||
|
name: string;
|
||||||
|
state: string;
|
||||||
|
ip: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const network = metricsWithNetwork?.network;
|
||||||
const interfaces = network?.interfaces || [];
|
const interfaces = network?.interfaces || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -30,7 +39,7 @@ export function NetworkWidget({ metrics }: NetworkWidgetProps) {
|
|||||||
<p className="text-sm">{t("serverStats.noInterfacesFound")}</p>
|
<p className="text-sm">{t("serverStats.noInterfacesFound")}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
interfaces.map((iface: any, index: number) => (
|
interfaces.map((iface, index: number) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="p-3 rounded-lg bg-dark-bg/50 border border-dark-border/30 hover:bg-dark-bg/60 transition-colors"
|
className="p-3 rounded-lg bg-dark-bg/50 border border-dark-border/30 hover:bg-dark-bg/60 transition-colors"
|
||||||
|
|||||||
@@ -11,7 +11,20 @@ interface ProcessesWidgetProps {
|
|||||||
export function ProcessesWidget({ metrics }: ProcessesWidgetProps) {
|
export function ProcessesWidget({ metrics }: ProcessesWidgetProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const processes = (metrics as any)?.processes;
|
const metricsWithProcesses = metrics as ServerMetrics & {
|
||||||
|
processes?: {
|
||||||
|
total?: number;
|
||||||
|
running?: number;
|
||||||
|
top?: Array<{
|
||||||
|
pid: number;
|
||||||
|
cpu: number;
|
||||||
|
mem: number;
|
||||||
|
command: string;
|
||||||
|
user: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const processes = metricsWithProcesses?.processes;
|
||||||
const topProcesses = processes?.top || [];
|
const topProcesses = processes?.top || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -46,7 +59,7 @@ export function ProcessesWidget({ metrics }: ProcessesWidgetProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{topProcesses.map((proc: any, index: number) => (
|
{topProcesses.map((proc, index: number) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="p-2.5 rounded-lg bg-dark-bg/30 hover:bg-dark-bg/50 transition-colors border border-dark-border/20"
|
className="p-2.5 rounded-lg bg-dark-bg/30 hover:bg-dark-bg/50 transition-colors border border-dark-border/20"
|
||||||
|
|||||||
@@ -11,7 +11,14 @@ interface SystemWidgetProps {
|
|||||||
export function SystemWidget({ metrics }: SystemWidgetProps) {
|
export function SystemWidget({ metrics }: SystemWidgetProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const system = (metrics as any)?.system;
|
const metricsWithSystem = metrics as ServerMetrics & {
|
||||||
|
system?: {
|
||||||
|
hostname?: string;
|
||||||
|
os?: string;
|
||||||
|
kernel?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const system = metricsWithSystem?.system;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
||||||
|
|||||||
@@ -11,7 +11,13 @@ interface UptimeWidgetProps {
|
|||||||
export function UptimeWidget({ metrics }: UptimeWidgetProps) {
|
export function UptimeWidget({ metrics }: UptimeWidgetProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const uptime = (metrics as any)?.uptime;
|
const metricsWithUptime = metrics as ServerMetrics & {
|
||||||
|
uptime?: {
|
||||||
|
formatted?: string;
|
||||||
|
seconds?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const uptime = metricsWithUptime?.uptime;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ interface SSHTunnelViewerProps {
|
|||||||
action: "connect" | "disconnect" | "cancel",
|
action: "connect" | "disconnect" | "cancel",
|
||||||
host: SSHHost,
|
host: SSHHost,
|
||||||
tunnelIndex: number,
|
tunnelIndex: number,
|
||||||
) => Promise<any>;
|
) => Promise<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TunnelViewer({
|
export function TunnelViewer({
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ export function HomepageAuth({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let res, meRes;
|
let res;
|
||||||
if (tab === "login") {
|
if (tab === "login") {
|
||||||
res = await loginUser(localUsername, password);
|
res = await loginUser(localUsername, password);
|
||||||
} else {
|
} else {
|
||||||
@@ -194,7 +194,7 @@ export function HomepageAuth({
|
|||||||
throw new Error(t("errors.loginFailed"));
|
throw new Error(t("errors.loginFailed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[meRes] = await Promise.all([getUserInfo()]);
|
const [meRes] = await Promise.all([getUserInfo()]);
|
||||||
|
|
||||||
setInternalLoggedIn(true);
|
setInternalLoggedIn(true);
|
||||||
setLoggedIn(true);
|
setLoggedIn(true);
|
||||||
@@ -217,16 +217,22 @@ export function HomepageAuth({
|
|||||||
setTotpRequired(false);
|
setTotpRequired(false);
|
||||||
setTotpCode("");
|
setTotpCode("");
|
||||||
setTotpTempToken("");
|
setTotpTempToken("");
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
|
const error = err as {
|
||||||
|
message?: string;
|
||||||
|
response?: { data?: { error?: string } };
|
||||||
|
};
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
err?.response?.data?.error || err?.message || t("errors.unknownError");
|
error?.response?.data?.error ||
|
||||||
|
error?.message ||
|
||||||
|
t("errors.unknownError");
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
setInternalLoggedIn(false);
|
setInternalLoggedIn(false);
|
||||||
setLoggedIn(false);
|
setLoggedIn(false);
|
||||||
setIsAdmin(false);
|
setIsAdmin(false);
|
||||||
setUsername(null);
|
setUsername(null);
|
||||||
setUserId(null);
|
setUserId(null);
|
||||||
if (err?.response?.data?.error?.includes("Database")) {
|
if (error?.response?.data?.error?.includes("Database")) {
|
||||||
setDbConnectionFailed(true);
|
setDbConnectionFailed(true);
|
||||||
} else {
|
} else {
|
||||||
setDbError(null);
|
setDbError(null);
|
||||||
@@ -242,10 +248,14 @@ export function HomepageAuth({
|
|||||||
await initiatePasswordReset(localUsername);
|
await initiatePasswordReset(localUsername);
|
||||||
setResetStep("verify");
|
setResetStep("verify");
|
||||||
toast.success(t("messages.resetCodeSent"));
|
toast.success(t("messages.resetCodeSent"));
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
|
const error = err as {
|
||||||
|
message?: string;
|
||||||
|
response?: { data?: { error?: string } };
|
||||||
|
};
|
||||||
toast.error(
|
toast.error(
|
||||||
err?.response?.data?.error ||
|
error?.response?.data?.error ||
|
||||||
err?.message ||
|
error?.message ||
|
||||||
t("errors.failedPasswordReset"),
|
t("errors.failedPasswordReset"),
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -260,8 +270,9 @@ export function HomepageAuth({
|
|||||||
setTempToken(response.tempToken);
|
setTempToken(response.tempToken);
|
||||||
setResetStep("newPassword");
|
setResetStep("newPassword");
|
||||||
toast.success(t("messages.codeVerified"));
|
toast.success(t("messages.codeVerified"));
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
toast.error(err?.response?.data?.error || t("errors.failedVerifyCode"));
|
const error = err as { response?: { data?: { error?: string } } };
|
||||||
|
toast.error(error?.response?.data?.error || t("errors.failedVerifyCode"));
|
||||||
} finally {
|
} finally {
|
||||||
setResetLoading(false);
|
setResetLoading(false);
|
||||||
}
|
}
|
||||||
@@ -296,9 +307,10 @@ export function HomepageAuth({
|
|||||||
|
|
||||||
setTab("login");
|
setTab("login");
|
||||||
resetPasswordState();
|
resetPasswordState();
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
|
const error = err as { response?: { data?: { error?: string } } };
|
||||||
toast.error(
|
toast.error(
|
||||||
err?.response?.data?.error || t("errors.failedCompleteReset"),
|
error?.response?.data?.error || t("errors.failedCompleteReset"),
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setResetLoading(false);
|
setResetLoading(false);
|
||||||
@@ -359,11 +371,15 @@ export function HomepageAuth({
|
|||||||
setTotpCode("");
|
setTotpCode("");
|
||||||
setTotpTempToken("");
|
setTotpTempToken("");
|
||||||
toast.success(t("messages.loginSuccess"));
|
toast.success(t("messages.loginSuccess"));
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
const errorCode = err?.response?.data?.code;
|
const error = err as {
|
||||||
|
message?: string;
|
||||||
|
response?: { data?: { code?: string; error?: string } };
|
||||||
|
};
|
||||||
|
const errorCode = error?.response?.data?.code;
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
err?.response?.data?.error ||
|
error?.response?.data?.error ||
|
||||||
err?.message ||
|
error?.message ||
|
||||||
t("errors.invalidTotpCode");
|
t("errors.invalidTotpCode");
|
||||||
|
|
||||||
if (errorCode === "SESSION_EXPIRED") {
|
if (errorCode === "SESSION_EXPIRED") {
|
||||||
@@ -391,10 +407,14 @@ export function HomepageAuth({
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.location.replace(authUrl);
|
window.location.replace(authUrl);
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
|
const error = err as {
|
||||||
|
message?: string;
|
||||||
|
response?: { data?: { error?: string } };
|
||||||
|
};
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
err?.response?.data?.error ||
|
error?.response?.data?.error ||
|
||||||
err?.message ||
|
error?.message ||
|
||||||
t("errors.failedOidcLogin");
|
t("errors.failedOidcLogin");
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
setOidcLoading(false);
|
setOidcLoading(false);
|
||||||
|
|||||||
@@ -23,7 +23,14 @@ interface SSHHost {
|
|||||||
enableTunnel: boolean;
|
enableTunnel: boolean;
|
||||||
enableFileManager: boolean;
|
enableFileManager: boolean;
|
||||||
defaultPath: string;
|
defaultPath: string;
|
||||||
tunnelConnections: any[];
|
tunnelConnections: Array<{
|
||||||
|
sourcePort: number;
|
||||||
|
endpointPort: number;
|
||||||
|
endpointHost: string;
|
||||||
|
maxRetries: number;
|
||||||
|
retryInterval: number;
|
||||||
|
autoStart: boolean;
|
||||||
|
}>;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ export function Host({ host }: HostProps): React.ReactElement {
|
|||||||
: `${host.username}@${host.ip}:${host.port}`;
|
: `${host.username}@${host.ip}:${host.port}`;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let intervalId: number | undefined;
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
const fetchStatus = async () => {
|
const fetchStatus = async () => {
|
||||||
@@ -29,13 +28,14 @@ export function Host({ host }: HostProps): React.ReactElement {
|
|||||||
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("degraded");
|
setServerStatus("degraded");
|
||||||
} else if (error?.response?.status === 404) {
|
} else if (err?.response?.status === 404) {
|
||||||
setServerStatus("offline");
|
setServerStatus("offline");
|
||||||
} else {
|
} else {
|
||||||
setServerStatus("offline");
|
setServerStatus("offline");
|
||||||
@@ -46,7 +46,7 @@ export function Host({ host }: HostProps): React.ReactElement {
|
|||||||
|
|
||||||
fetchStatus();
|
fetchStatus();
|
||||||
|
|
||||||
intervalId = window.setInterval(fetchStatus, 30000);
|
const intervalId = window.setInterval(fetchStatus, 30000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
|
|||||||
@@ -19,7 +19,16 @@ interface TabContextType {
|
|||||||
setCurrentTab: (tabId: number) => void;
|
setCurrentTab: (tabId: number) => void;
|
||||||
setSplitScreenTab: (tabId: number) => void;
|
setSplitScreenTab: (tabId: number) => void;
|
||||||
getTab: (tabId: number) => Tab | undefined;
|
getTab: (tabId: number) => Tab | undefined;
|
||||||
updateHostConfig: (hostId: number, newHostConfig: any) => void;
|
updateHostConfig: (
|
||||||
|
hostId: number,
|
||||||
|
newHostConfig: {
|
||||||
|
id: number;
|
||||||
|
name?: string;
|
||||||
|
username: string;
|
||||||
|
ip: string;
|
||||||
|
port: number;
|
||||||
|
},
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TabContext = createContext<TabContextType | undefined>(undefined);
|
const TabContext = createContext<TabContextType | undefined>(undefined);
|
||||||
@@ -98,7 +107,9 @@ export function TabProvider({ children }: TabProviderProps) {
|
|||||||
id,
|
id,
|
||||||
title: effectiveTitle,
|
title: effectiveTitle,
|
||||||
terminalRef:
|
terminalRef:
|
||||||
tabData.type === "terminal" ? React.createRef<any>() : undefined,
|
tabData.type === "terminal"
|
||||||
|
? React.createRef<{ disconnect?: () => void }>()
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
setTabs((prev) => [...prev, newTab]);
|
setTabs((prev) => [...prev, newTab]);
|
||||||
setCurrentTab(id);
|
setCurrentTab(id);
|
||||||
@@ -140,7 +151,16 @@ export function TabProvider({ children }: TabProviderProps) {
|
|||||||
return tabs.find((tab) => tab.id === tabId);
|
return tabs.find((tab) => tab.id === tabId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateHostConfig = (hostId: number, newHostConfig: any) => {
|
const updateHostConfig = (
|
||||||
|
hostId: number,
|
||||||
|
newHostConfig: {
|
||||||
|
id: number;
|
||||||
|
name?: string;
|
||||||
|
username: string;
|
||||||
|
ip: string;
|
||||||
|
port: number;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
setTabs((prev) =>
|
setTabs((prev) =>
|
||||||
prev.map((tab) => {
|
prev.map((tab) => {
|
||||||
if (tab.hostConfig && tab.hostConfig.id === hostId) {
|
if (tab.hostConfig && tab.hostConfig.id === hostId) {
|
||||||
|
|||||||
@@ -49,10 +49,14 @@ export function PasswordReset({ userInfo }: PasswordResetProps) {
|
|||||||
await initiatePasswordReset(userInfo.username);
|
await initiatePasswordReset(userInfo.username);
|
||||||
setResetStep("verify");
|
setResetStep("verify");
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
|
const error = err as {
|
||||||
|
message?: string;
|
||||||
|
response?: { data?: { error?: string } };
|
||||||
|
};
|
||||||
setError(
|
setError(
|
||||||
err?.response?.data?.error ||
|
error?.response?.data?.error ||
|
||||||
err?.message ||
|
error?.message ||
|
||||||
t("common.failedToInitiatePasswordReset"),
|
t("common.failedToInitiatePasswordReset"),
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -80,9 +84,10 @@ export function PasswordReset({ userInfo }: PasswordResetProps) {
|
|||||||
setTempToken(response.tempToken);
|
setTempToken(response.tempToken);
|
||||||
setResetStep("newPassword");
|
setResetStep("newPassword");
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
|
const error = err as { response?: { data?: { error?: string } } };
|
||||||
setError(
|
setError(
|
||||||
err?.response?.data?.error || t("common.failedToVerifyResetCode"),
|
error?.response?.data?.error || t("common.failedToVerifyResetCode"),
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setResetLoading(false);
|
setResetLoading(false);
|
||||||
@@ -110,9 +115,11 @@ export function PasswordReset({ userInfo }: PasswordResetProps) {
|
|||||||
|
|
||||||
toast.success(t("common.passwordResetSuccess"));
|
toast.success(t("common.passwordResetSuccess"));
|
||||||
resetPasswordState();
|
resetPasswordState();
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
|
const error = err as { response?: { data?: { error?: string } } };
|
||||||
setError(
|
setError(
|
||||||
err?.response?.data?.error || t("common.failedToCompletePasswordReset"),
|
error?.response?.data?.error ||
|
||||||
|
t("common.failedToCompletePasswordReset"),
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setResetLoading(false);
|
setResetLoading(false);
|
||||||
|
|||||||
@@ -66,8 +66,9 @@ export function TOTPSetup({
|
|||||||
setSecret(response.secret);
|
setSecret(response.secret);
|
||||||
setSetupStep("qr");
|
setSetupStep("qr");
|
||||||
setIsSettingUp(true);
|
setIsSettingUp(true);
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
setError(err?.response?.data?.error || "Failed to start TOTP setup");
|
const error = err as { response?: { data?: { error?: string } } };
|
||||||
|
setError(error?.response?.data?.error || "Failed to start TOTP setup");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -86,8 +87,9 @@ export function TOTPSetup({
|
|||||||
setBackupCodes(response.backup_codes);
|
setBackupCodes(response.backup_codes);
|
||||||
setSetupStep("backup");
|
setSetupStep("backup");
|
||||||
toast.success(t("auth.twoFactorEnabledSuccess"));
|
toast.success(t("auth.twoFactorEnabledSuccess"));
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
setError(err?.response?.data?.error || "Invalid verification code");
|
const error = err as { response?: { data?: { error?: string } } };
|
||||||
|
setError(error?.response?.data?.error || "Invalid verification code");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -105,8 +107,9 @@ export function TOTPSetup({
|
|||||||
setDisableCode("");
|
setDisableCode("");
|
||||||
onStatusChange?.(false);
|
onStatusChange?.(false);
|
||||||
toast.success(t("auth.twoFactorDisabled"));
|
toast.success(t("auth.twoFactorDisabled"));
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
setError(err?.response?.data?.error || "Failed to disable TOTP");
|
const error = err as { response?: { data?: { error?: string } } };
|
||||||
|
setError(error?.response?.data?.error || "Failed to disable TOTP");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -122,8 +125,11 @@ export function TOTPSetup({
|
|||||||
);
|
);
|
||||||
setBackupCodes(response.backup_codes);
|
setBackupCodes(response.backup_codes);
|
||||||
toast.success(t("auth.newBackupCodesGenerated"));
|
toast.success(t("auth.newBackupCodesGenerated"));
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
setError(err?.response?.data?.error || "Failed to generate backup codes");
|
const error = err as { response?: { data?: { error?: string } } };
|
||||||
|
setError(
|
||||||
|
error?.response?.data?.error || "Failed to generate backup codes",
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,8 +62,9 @@ export function UserProfile({ isTopbarOpen = true }: UserProfileProps) {
|
|||||||
is_oidc: info.is_oidc,
|
is_oidc: info.is_oidc,
|
||||||
totp_enabled: info.totp_enabled || false,
|
totp_enabled: info.totp_enabled || false,
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
setError(err?.response?.data?.error || t("errors.loadFailed"));
|
const error = err as { response?: { data?: { error?: string } } };
|
||||||
|
setError(error?.response?.data?.error || t("errors.loadFailed"));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,14 @@ interface SSHHost {
|
|||||||
enableTunnel: boolean;
|
enableTunnel: boolean;
|
||||||
enableFileManager: boolean;
|
enableFileManager: boolean;
|
||||||
defaultPath: string;
|
defaultPath: string;
|
||||||
tunnelConnections: any[];
|
tunnelConnections: Array<{
|
||||||
|
sourcePort: number;
|
||||||
|
endpointPort: number;
|
||||||
|
endpointHost: string;
|
||||||
|
maxRetries: number;
|
||||||
|
retryInterval: number;
|
||||||
|
autoStart: boolean;
|
||||||
|
}>;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ export function Host({ host, onHostConnect }: HostProps): React.ReactElement {
|
|||||||
: `${host.username}@${host.ip}:${host.port}`;
|
: `${host.username}@${host.ip}:${host.port}`;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let intervalId: number | undefined;
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
const fetchStatus = async () => {
|
const fetchStatus = async () => {
|
||||||
@@ -29,13 +28,14 @@ export function Host({ host, onHostConnect }: HostProps): React.ReactElement {
|
|||||||
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("degraded");
|
setServerStatus("degraded");
|
||||||
} else if (error?.response?.status === 404) {
|
} else if (err?.response?.status === 404) {
|
||||||
setServerStatus("offline");
|
setServerStatus("offline");
|
||||||
} else {
|
} else {
|
||||||
setServerStatus("offline");
|
setServerStatus("offline");
|
||||||
@@ -46,7 +46,7 @@ export function Host({ host, onHostConnect }: HostProps): React.ReactElement {
|
|||||||
|
|
||||||
fetchStatus();
|
fetchStatus();
|
||||||
|
|
||||||
intervalId = window.setInterval(fetchStatus, 30000);
|
const intervalId = window.setInterval(fetchStatus, 30000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
|
|||||||
@@ -42,7 +42,14 @@ interface SSHHost {
|
|||||||
enableTunnel: boolean;
|
enableTunnel: boolean;
|
||||||
enableFileManager: boolean;
|
enableFileManager: boolean;
|
||||||
defaultPath: string;
|
defaultPath: string;
|
||||||
tunnelConnections: any[];
|
tunnelConnections: Array<{
|
||||||
|
sourcePort: number;
|
||||||
|
endpointPort: number;
|
||||||
|
endpointHost: string;
|
||||||
|
maxRetries: number;
|
||||||
|
retryInterval: number;
|
||||||
|
autoStart: boolean;
|
||||||
|
}>;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export function TabProvider({ children }: TabProviderProps) {
|
|||||||
...tabData,
|
...tabData,
|
||||||
id,
|
id,
|
||||||
title: computeUniqueTitle(tabData.title),
|
title: computeUniqueTitle(tabData.title),
|
||||||
terminalRef: React.createRef<any>(),
|
terminalRef: React.createRef<{ disconnect?: () => void }>(),
|
||||||
};
|
};
|
||||||
setTabs((prev) => [...prev, newTab]);
|
setTabs((prev) => [...prev, newTab]);
|
||||||
setCurrentTab(id);
|
setCurrentTab(id);
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ export function HomepageAuth({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let res, meRes;
|
let res;
|
||||||
if (tab === "login") {
|
if (tab === "login") {
|
||||||
res = await loginUser(localUsername, password);
|
res = await loginUser(localUsername, password);
|
||||||
} else {
|
} else {
|
||||||
@@ -192,7 +192,7 @@ export function HomepageAuth({
|
|||||||
throw new Error(t("errors.loginFailed"));
|
throw new Error(t("errors.loginFailed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[meRes] = await Promise.all([getUserInfo()]);
|
const [meRes] = await Promise.all([getUserInfo()]);
|
||||||
|
|
||||||
setInternalLoggedIn(true);
|
setInternalLoggedIn(true);
|
||||||
setLoggedIn(true);
|
setLoggedIn(true);
|
||||||
@@ -215,16 +215,22 @@ export function HomepageAuth({
|
|||||||
setTotpRequired(false);
|
setTotpRequired(false);
|
||||||
setTotpCode("");
|
setTotpCode("");
|
||||||
setTotpTempToken("");
|
setTotpTempToken("");
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
|
const error = err as {
|
||||||
|
message?: string;
|
||||||
|
response?: { data?: { error?: string } };
|
||||||
|
};
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
err?.response?.data?.error || err?.message || t("errors.unknownError");
|
error?.response?.data?.error ||
|
||||||
|
error?.message ||
|
||||||
|
t("errors.unknownError");
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
setInternalLoggedIn(false);
|
setInternalLoggedIn(false);
|
||||||
setLoggedIn(false);
|
setLoggedIn(false);
|
||||||
setIsAdmin(false);
|
setIsAdmin(false);
|
||||||
setUsername(null);
|
setUsername(null);
|
||||||
setUserId(null);
|
setUserId(null);
|
||||||
if (err?.response?.data?.error?.includes("Database")) {
|
if (error?.response?.data?.error?.includes("Database")) {
|
||||||
setDbError(t("errors.databaseConnection"));
|
setDbError(t("errors.databaseConnection"));
|
||||||
} else {
|
} else {
|
||||||
setDbError(null);
|
setDbError(null);
|
||||||
@@ -241,10 +247,14 @@ export function HomepageAuth({
|
|||||||
await initiatePasswordReset(localUsername);
|
await initiatePasswordReset(localUsername);
|
||||||
setResetStep("verify");
|
setResetStep("verify");
|
||||||
toast.success(t("messages.resetCodeSent"));
|
toast.success(t("messages.resetCodeSent"));
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
|
const error = err as {
|
||||||
|
message?: string;
|
||||||
|
response?: { data?: { error?: string } };
|
||||||
|
};
|
||||||
toast.error(
|
toast.error(
|
||||||
err?.response?.data?.error ||
|
error?.response?.data?.error ||
|
||||||
err?.message ||
|
error?.message ||
|
||||||
t("errors.failedPasswordReset"),
|
t("errors.failedPasswordReset"),
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -260,8 +270,9 @@ export function HomepageAuth({
|
|||||||
setTempToken(response.tempToken);
|
setTempToken(response.tempToken);
|
||||||
setResetStep("newPassword");
|
setResetStep("newPassword");
|
||||||
toast.success(t("messages.codeVerified"));
|
toast.success(t("messages.codeVerified"));
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
toast.error(err?.response?.data?.error || t("errors.failedVerifyCode"));
|
const error = err as { response?: { data?: { error?: string } } };
|
||||||
|
toast.error(error?.response?.data?.error || t("errors.failedVerifyCode"));
|
||||||
} finally {
|
} finally {
|
||||||
setResetLoading(false);
|
setResetLoading(false);
|
||||||
}
|
}
|
||||||
@@ -298,9 +309,10 @@ export function HomepageAuth({
|
|||||||
|
|
||||||
setTab("login");
|
setTab("login");
|
||||||
resetPasswordState();
|
resetPasswordState();
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
|
const error = err as { response?: { data?: { error?: string } } };
|
||||||
toast.error(
|
toast.error(
|
||||||
err?.response?.data?.error || t("errors.failedCompleteReset"),
|
error?.response?.data?.error || t("errors.failedCompleteReset"),
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setResetLoading(false);
|
setResetLoading(false);
|
||||||
@@ -364,11 +376,15 @@ export function HomepageAuth({
|
|||||||
setTotpCode("");
|
setTotpCode("");
|
||||||
setTotpTempToken("");
|
setTotpTempToken("");
|
||||||
toast.success(t("messages.loginSuccess"));
|
toast.success(t("messages.loginSuccess"));
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
const errorCode = err?.response?.data?.code;
|
const error = err as {
|
||||||
|
message?: string;
|
||||||
|
response?: { data?: { code?: string; error?: string } };
|
||||||
|
};
|
||||||
|
const errorCode = error?.response?.data?.code;
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
err?.response?.data?.error ||
|
error?.response?.data?.error ||
|
||||||
err?.message ||
|
error?.message ||
|
||||||
t("errors.invalidTotpCode");
|
t("errors.invalidTotpCode");
|
||||||
|
|
||||||
if (errorCode === "SESSION_EXPIRED") {
|
if (errorCode === "SESSION_EXPIRED") {
|
||||||
@@ -397,10 +413,14 @@ export function HomepageAuth({
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.location.replace(authUrl);
|
window.location.replace(authUrl);
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
|
const error = err as {
|
||||||
|
message?: string;
|
||||||
|
response?: { data?: { error?: string } };
|
||||||
|
};
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
err?.response?.data?.error ||
|
error?.response?.data?.error ||
|
||||||
err?.message ||
|
error?.message ||
|
||||||
t("errors.failedOidcLogin");
|
t("errors.failedOidcLogin");
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
setOidcLoading(false);
|
setOidcLoading(false);
|
||||||
|
|||||||
@@ -23,7 +23,14 @@ interface SSHHost {
|
|||||||
enableTunnel: boolean;
|
enableTunnel: boolean;
|
||||||
enableFileManager: boolean;
|
enableFileManager: boolean;
|
||||||
defaultPath: string;
|
defaultPath: string;
|
||||||
tunnelConnections: any[];
|
tunnelConnections: Array<{
|
||||||
|
sourcePort: number;
|
||||||
|
endpointPort: number;
|
||||||
|
endpointHost: string;
|
||||||
|
maxRetries: number;
|
||||||
|
retryInterval: number;
|
||||||
|
autoStart: boolean;
|
||||||
|
}>;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ export function Host({ host, onHostConnect }: HostProps): React.ReactElement {
|
|||||||
: `${host.username}@${host.ip}:${host.port}`;
|
: `${host.username}@${host.ip}:${host.port}`;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let intervalId: number | undefined;
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
const fetchStatus = async () => {
|
const fetchStatus = async () => {
|
||||||
@@ -29,13 +28,14 @@ export function Host({ host, onHostConnect }: HostProps): React.ReactElement {
|
|||||||
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("degraded");
|
setServerStatus("degraded");
|
||||||
} else if (error?.response?.status === 404) {
|
} else if (err?.response?.status === 404) {
|
||||||
setServerStatus("offline");
|
setServerStatus("offline");
|
||||||
} else {
|
} else {
|
||||||
setServerStatus("offline");
|
setServerStatus("offline");
|
||||||
@@ -46,7 +46,7 @@ export function Host({ host, onHostConnect }: HostProps): React.ReactElement {
|
|||||||
|
|
||||||
fetchStatus();
|
fetchStatus();
|
||||||
|
|
||||||
intervalId = window.setInterval(fetchStatus, 30000);
|
const intervalId = window.setInterval(fetchStatus, 30000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
|
|||||||
@@ -43,7 +43,14 @@ interface SSHHost {
|
|||||||
enableTunnel: boolean;
|
enableTunnel: boolean;
|
||||||
enableFileManager: boolean;
|
enableFileManager: boolean;
|
||||||
defaultPath: string;
|
defaultPath: string;
|
||||||
tunnelConnections: any[];
|
tunnelConnections: Array<{
|
||||||
|
sourcePort: number;
|
||||||
|
endpointPort: number;
|
||||||
|
endpointHost: string;
|
||||||
|
maxRetries: number;
|
||||||
|
retryInterval: number;
|
||||||
|
autoStart: boolean;
|
||||||
|
}>;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export function TabProvider({ children }: TabProviderProps) {
|
|||||||
...tabData,
|
...tabData,
|
||||||
id,
|
id,
|
||||||
title: computeUniqueTitle(tabData.title),
|
title: computeUniqueTitle(tabData.title),
|
||||||
terminalRef: React.createRef<any>(),
|
terminalRef: React.createRef<{ disconnect?: () => void }>(),
|
||||||
};
|
};
|
||||||
setTabs((prev) => [...prev, newTab]);
|
setTabs((prev) => [...prev, newTab]);
|
||||||
setCurrentTab(id);
|
setCurrentTab(id);
|
||||||
|
|||||||
@@ -114,9 +114,10 @@ export function useDragToDesktop({ sshSessionId }: UseDragToDesktopProps) {
|
|||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error("Failed to drag to desktop:", error);
|
console.error("Failed to drag to desktop:", error);
|
||||||
const errorMessage = error.message || "Drag failed";
|
const err = error as { message?: string };
|
||||||
|
const errorMessage = err.message || "Drag failed";
|
||||||
|
|
||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -223,9 +224,10 @@ export function useDragToDesktop({ sshSessionId }: UseDragToDesktopProps) {
|
|||||||
}));
|
}));
|
||||||
}, 15000);
|
}, 15000);
|
||||||
return true;
|
return true;
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error("Failed to batch drag to desktop:", error);
|
console.error("Failed to batch drag to desktop:", error);
|
||||||
const errorMessage = error.message || "Batch drag failed";
|
const err = error as { message?: string };
|
||||||
|
const errorMessage = err.message || "Batch drag failed";
|
||||||
|
|
||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ export function useDragToSystemDesktop({ sshSessionId }: UseDragToSystemProps) {
|
|||||||
options: DragToSystemOptions;
|
options: DragToSystemOptions;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const saveLastDirectory = async (fileHandle: any) => {
|
const saveLastDirectory = async (fileHandle: {
|
||||||
|
getParent?: () => Promise<unknown>;
|
||||||
|
}) => {
|
||||||
try {
|
try {
|
||||||
if ("indexedDB" in window && fileHandle.getParent) {
|
if ("indexedDB" in window && fileHandle.getParent) {
|
||||||
const dirHandle = await fileHandle.getParent();
|
const dirHandle = await fileHandle.getParent();
|
||||||
@@ -133,10 +135,33 @@ export function useDragToSystemDesktop({ sshSessionId }: UseDragToSystemProps) {
|
|||||||
const fileName =
|
const fileName =
|
||||||
fileList.length === 1 ? fileList[0].name : `files_${Date.now()}.zip`;
|
fileList.length === 1 ? fileList[0].name : `files_${Date.now()}.zip`;
|
||||||
|
|
||||||
let fileHandle: any = null;
|
let fileHandle: {
|
||||||
|
createWritable?: () => Promise<{
|
||||||
|
write: (data: Blob) => Promise<void>;
|
||||||
|
close: () => Promise<void>;
|
||||||
|
}>;
|
||||||
|
getParent?: () => Promise<unknown>;
|
||||||
|
} | null = null;
|
||||||
if (isFileSystemAPISupported()) {
|
if (isFileSystemAPISupported()) {
|
||||||
try {
|
try {
|
||||||
fileHandle = await (window as any).showSaveFilePicker({
|
fileHandle = await (
|
||||||
|
window as Window & {
|
||||||
|
showSaveFilePicker?: (options: {
|
||||||
|
suggestedName: string;
|
||||||
|
startIn: string;
|
||||||
|
types: Array<{
|
||||||
|
description: string;
|
||||||
|
accept: Record<string, string[]>;
|
||||||
|
}>;
|
||||||
|
}) => Promise<{
|
||||||
|
createWritable?: () => Promise<{
|
||||||
|
write: (data: Blob) => Promise<void>;
|
||||||
|
close: () => Promise<void>;
|
||||||
|
}>;
|
||||||
|
getParent?: () => Promise<unknown>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
).showSaveFilePicker!({
|
||||||
suggestedName: fileName,
|
suggestedName: fileName,
|
||||||
startIn: "desktop",
|
startIn: "desktop",
|
||||||
types: [
|
types: [
|
||||||
@@ -156,8 +181,9 @@ export function useDragToSystemDesktop({ sshSessionId }: UseDragToSystemProps) {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
if (error.name === "AbortError") {
|
const err = error as { name?: string };
|
||||||
|
if (err.name === "AbortError") {
|
||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
isDownloading: false,
|
isDownloading: false,
|
||||||
@@ -211,8 +237,9 @@ export function useDragToSystemDesktop({ sshSessionId }: UseDragToSystemProps) {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
const errorMessage = error.message || "Save failed";
|
const err = error as { message?: string };
|
||||||
|
const errorMessage = err.message || "Save failed";
|
||||||
|
|
||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|||||||
Reference in New Issue
Block a user