mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 22:55:24 +00:00
291 lines
8.2 KiB
TypeScript
291 lines
8.2 KiB
TypeScript
import { sql } from "drizzle-orm";
|
|
import {
|
|
createCurrentDismissedAlertRepository,
|
|
createCurrentUserDataExportRepository,
|
|
createCurrentUserRepository,
|
|
} from "../database/repositories/factory.js";
|
|
import { getDb } from "../database/db/index.js";
|
|
import { DataCrypto } from "./data-crypto.js";
|
|
import { databaseLogger } from "./logger.js";
|
|
|
|
// The file manager plugin owns these tables now (p_file_manager_*); read by
|
|
// raw SQL rather than importing plugin code into core.
|
|
async function listFileManagerExportData(userId: string): Promise<{
|
|
recent: unknown[];
|
|
pinned: unknown[];
|
|
shortcuts: unknown[];
|
|
transferRecent: unknown[];
|
|
}> {
|
|
const db = getDb();
|
|
const [recent, pinned, shortcuts, transferRecent] = await Promise.all([
|
|
db.all(sql`SELECT * FROM p_file_manager_recent WHERE user_id = ${userId}`),
|
|
db.all(sql`SELECT * FROM p_file_manager_pinned WHERE user_id = ${userId}`),
|
|
db.all(
|
|
sql`SELECT * FROM p_file_manager_shortcuts WHERE user_id = ${userId}`,
|
|
),
|
|
db.all(
|
|
sql`SELECT * FROM p_file_manager_transfer_recent WHERE user_id = ${userId}`,
|
|
),
|
|
]);
|
|
return { recent, pinned, shortcuts, transferRecent };
|
|
}
|
|
|
|
interface UserExportData {
|
|
version: string;
|
|
exportedAt: string;
|
|
userId: string;
|
|
username: string;
|
|
userData: {
|
|
sshHosts: unknown[];
|
|
sshCredentials: unknown[];
|
|
fileManagerData: {
|
|
recent: unknown[];
|
|
pinned: unknown[];
|
|
shortcuts: unknown[];
|
|
transferRecent: 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 userRecord = await createCurrentUserRepository().findById(userId);
|
|
if (!userRecord) {
|
|
throw new Error(`User not found: ${userId}`);
|
|
}
|
|
|
|
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 exportRepository = createCurrentUserDataExportRepository();
|
|
const sshHosts = await exportRepository.listHostsByUserId(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 exportRepository.listCredentialsByUserId(userId);
|
|
sshCredentialsData =
|
|
format === "plaintext" && userDataKey
|
|
? credentials.map((cred) =>
|
|
DataCrypto.decryptRecord(
|
|
"ssh_credentials",
|
|
cred,
|
|
userId,
|
|
userDataKey!,
|
|
),
|
|
)
|
|
: credentials;
|
|
}
|
|
|
|
const {
|
|
recent: recentFiles,
|
|
pinned: pinnedFiles,
|
|
shortcuts,
|
|
transferRecent: transferRecentData,
|
|
} = await listFileManagerExportData(userId);
|
|
|
|
const alerts =
|
|
await createCurrentDismissedAlertRepository().listByUserId(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,
|
|
transferRecent: transferRecentData,
|
|
},
|
|
dismissedAlerts: alerts,
|
|
},
|
|
metadata: {
|
|
totalRecords:
|
|
processedSshHosts.length +
|
|
sshCredentialsData.length +
|
|
recentFiles.length +
|
|
pinnedFiles.length +
|
|
shortcuts.length +
|
|
transferRecentData.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 };
|