- Replace 'any' with 'unknown' in catch blocks and add type assertions - Create explicit interfaces for complex objects (HostConfig, TabData, TerminalHandle) - Fix window/document object type extensions - Update Electron API type definitions - Improve type safety in database routes and utilities - Add proper types to Terminal components (Desktop & Mobile) - Fix navigation component types (TopNavbar, LeftSidebar, AppView) Reduces TypeScript lint errors from 394 to 358 (-36 errors) Fixes 45 @typescript-eslint/no-explicit-any violations
289 lines
7.7 KiB
TypeScript
289 lines
7.7 KiB
TypeScript
import { getDb } from "../database/db/index.js";
|
|
import {
|
|
users,
|
|
sshData,
|
|
sshCredentials,
|
|
fileManagerRecent,
|
|
fileManagerPinned,
|
|
fileManagerShortcuts,
|
|
dismissedAlerts,
|
|
} from "../database/db/schema.js";
|
|
import { eq } from "drizzle-orm";
|
|
import { DataCrypto } from "./data-crypto.js";
|
|
import { databaseLogger } from "./logger.js";
|
|
|
|
interface UserExportData {
|
|
version: string;
|
|
exportedAt: string;
|
|
userId: string;
|
|
username: string;
|
|
userData: {
|
|
sshHosts: unknown[];
|
|
sshCredentials: unknown[];
|
|
fileManagerData: {
|
|
recent: unknown[];
|
|
pinned: unknown[];
|
|
shortcuts: unknown[];
|
|
};
|
|
dismissedAlerts: unknown[];
|
|
};
|
|
metadata: {
|
|
totalRecords: number;
|
|
encrypted: boolean;
|
|
exportType: "user_data" | "system_config" | "all";
|
|
};
|
|
}
|
|
|
|
class UserDataExport {
|
|
private static readonly EXPORT_VERSION = "v2.0";
|
|
|
|
static async exportUserData(
|
|
userId: string,
|
|
options: {
|
|
format?: "encrypted" | "plaintext";
|
|
scope?: "user_data" | "all";
|
|
includeCredentials?: boolean;
|
|
} = {},
|
|
): Promise<UserExportData> {
|
|
const {
|
|
format = "encrypted",
|
|
scope = "user_data",
|
|
includeCredentials = true,
|
|
} = options;
|
|
|
|
try {
|
|
const user = await getDb()
|
|
.select()
|
|
.from(users)
|
|
.where(eq(users.id, userId));
|
|
if (!user || user.length === 0) {
|
|
throw new Error(`User not found: ${userId}`);
|
|
}
|
|
|
|
const userRecord = user[0];
|
|
|
|
let userDataKey: Buffer | null = null;
|
|
if (format === "plaintext") {
|
|
userDataKey = DataCrypto.getUserDataKey(userId);
|
|
if (!userDataKey) {
|
|
throw new Error(
|
|
"User data not unlocked - password required for plaintext export",
|
|
);
|
|
}
|
|
}
|
|
|
|
const sshHosts = await getDb()
|
|
.select()
|
|
.from(sshData)
|
|
.where(eq(sshData.userId, userId));
|
|
const processedSshHosts =
|
|
format === "plaintext" && userDataKey
|
|
? sshHosts.map((host) =>
|
|
DataCrypto.decryptRecord("ssh_data", host, userId, userDataKey!),
|
|
)
|
|
: sshHosts;
|
|
|
|
let sshCredentialsData: unknown[] = [];
|
|
if (includeCredentials) {
|
|
const credentials = await getDb()
|
|
.select()
|
|
.from(sshCredentials)
|
|
.where(eq(sshCredentials.userId, userId));
|
|
sshCredentialsData =
|
|
format === "plaintext" && userDataKey
|
|
? credentials.map((cred) =>
|
|
DataCrypto.decryptRecord(
|
|
"ssh_credentials",
|
|
cred,
|
|
userId,
|
|
userDataKey!,
|
|
),
|
|
)
|
|
: credentials;
|
|
}
|
|
|
|
const [recentFiles, pinnedFiles, shortcuts] = await Promise.all([
|
|
getDb()
|
|
.select()
|
|
.from(fileManagerRecent)
|
|
.where(eq(fileManagerRecent.userId, userId)),
|
|
getDb()
|
|
.select()
|
|
.from(fileManagerPinned)
|
|
.where(eq(fileManagerPinned.userId, userId)),
|
|
getDb()
|
|
.select()
|
|
.from(fileManagerShortcuts)
|
|
.where(eq(fileManagerShortcuts.userId, userId)),
|
|
]);
|
|
|
|
const alerts = await getDb()
|
|
.select()
|
|
.from(dismissedAlerts)
|
|
.where(eq(dismissedAlerts.userId, userId));
|
|
|
|
const exportData: UserExportData = {
|
|
version: this.EXPORT_VERSION,
|
|
exportedAt: new Date().toISOString(),
|
|
userId: userRecord.id,
|
|
username: userRecord.username,
|
|
userData: {
|
|
sshHosts: processedSshHosts,
|
|
sshCredentials: sshCredentialsData,
|
|
fileManagerData: {
|
|
recent: recentFiles,
|
|
pinned: pinnedFiles,
|
|
shortcuts: shortcuts,
|
|
},
|
|
dismissedAlerts: alerts,
|
|
},
|
|
metadata: {
|
|
totalRecords:
|
|
processedSshHosts.length +
|
|
sshCredentialsData.length +
|
|
recentFiles.length +
|
|
pinnedFiles.length +
|
|
shortcuts.length +
|
|
alerts.length,
|
|
encrypted: format === "encrypted",
|
|
exportType: scope,
|
|
},
|
|
};
|
|
|
|
databaseLogger.success("User data export completed", {
|
|
operation: "user_data_export_complete",
|
|
userId,
|
|
totalRecords: exportData.metadata.totalRecords,
|
|
format,
|
|
sshHosts: processedSshHosts.length,
|
|
sshCredentials: sshCredentialsData.length,
|
|
});
|
|
|
|
return exportData;
|
|
} catch (error) {
|
|
databaseLogger.error("User data export failed", error, {
|
|
operation: "user_data_export_failed",
|
|
userId,
|
|
format,
|
|
scope,
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
static async exportUserDataToJSON(
|
|
userId: string,
|
|
options: {
|
|
format?: "encrypted" | "plaintext";
|
|
scope?: "user_data" | "all";
|
|
includeCredentials?: boolean;
|
|
pretty?: boolean;
|
|
} = {},
|
|
): Promise<string> {
|
|
const { pretty = true } = options;
|
|
const exportData = await this.exportUserData(userId, options);
|
|
return JSON.stringify(exportData, null, pretty ? 2 : 0);
|
|
}
|
|
|
|
static validateExportData(data: unknown): {
|
|
valid: boolean;
|
|
errors: string[];
|
|
} {
|
|
const errors: string[] = [];
|
|
|
|
if (!data || typeof data !== "object") {
|
|
errors.push("Export data must be an object");
|
|
return { valid: false, errors };
|
|
}
|
|
|
|
const dataObj = data as Record<string, unknown>;
|
|
|
|
if (!dataObj.version) {
|
|
errors.push("Missing version field");
|
|
}
|
|
|
|
if (!dataObj.userId) {
|
|
errors.push("Missing userId field");
|
|
}
|
|
|
|
if (!dataObj.userData || typeof dataObj.userData !== "object") {
|
|
errors.push("Missing or invalid userData field");
|
|
}
|
|
|
|
if (!dataObj.metadata || typeof dataObj.metadata !== "object") {
|
|
errors.push("Missing or invalid metadata field");
|
|
}
|
|
|
|
if (dataObj.userData) {
|
|
const userData = dataObj.userData as Record<string, unknown>;
|
|
const requiredFields = [
|
|
"sshHosts",
|
|
"sshCredentials",
|
|
"fileManagerData",
|
|
"dismissedAlerts",
|
|
];
|
|
for (const field of requiredFields) {
|
|
if (
|
|
!Array.isArray(userData[field]) &&
|
|
!(field === "fileManagerData" && typeof userData[field] === "object")
|
|
) {
|
|
errors.push(`Missing or invalid userData.${field} field`);
|
|
}
|
|
}
|
|
|
|
if (
|
|
userData.fileManagerData &&
|
|
typeof userData.fileManagerData === "object"
|
|
) {
|
|
const fileManagerData = userData.fileManagerData as Record<
|
|
string,
|
|
unknown
|
|
>;
|
|
const fmFields = ["recent", "pinned", "shortcuts"];
|
|
for (const field of fmFields) {
|
|
if (!Array.isArray(fileManagerData[field])) {
|
|
errors.push(
|
|
`Missing or invalid userData.fileManagerData.${field} field`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return { valid: errors.length === 0, errors };
|
|
}
|
|
|
|
static getExportStats(data: UserExportData): {
|
|
version: string;
|
|
exportedAt: string;
|
|
username: string;
|
|
totalRecords: number;
|
|
breakdown: {
|
|
sshHosts: number;
|
|
sshCredentials: number;
|
|
fileManagerItems: number;
|
|
dismissedAlerts: number;
|
|
};
|
|
encrypted: boolean;
|
|
} {
|
|
return {
|
|
version: data.version,
|
|
exportedAt: data.exportedAt,
|
|
username: data.username,
|
|
totalRecords: data.metadata.totalRecords,
|
|
breakdown: {
|
|
sshHosts: data.userData.sshHosts.length,
|
|
sshCredentials: data.userData.sshCredentials.length,
|
|
fileManagerItems:
|
|
data.userData.fileManagerData.recent.length +
|
|
data.userData.fileManagerData.pinned.length +
|
|
data.userData.fileManagerData.shortcuts.length,
|
|
dismissedAlerts: data.userData.dismissedAlerts.length,
|
|
},
|
|
encrypted: data.metadata.encrypted,
|
|
};
|
|
}
|
|
}
|
|
|
|
export { UserDataExport, type UserExportData };
|