Files
Termix/src/backend/utils/user-data-export.ts
ZacharyZcR d7e98cda04 fix: replace explicit any types with proper TypeScript types
- 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
2025-10-09 18:06:17 +08:00

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