mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-16 05:13:36 +00:00
feat: initial ui redesign from demo
This commit is contained in:
@@ -30,6 +30,10 @@ class FieldCrypto {
|
||||
"autostartPassword",
|
||||
"autostartKey",
|
||||
"autostartKeyPassword",
|
||||
"socks5Password",
|
||||
"rdpPassword",
|
||||
"vncPassword",
|
||||
"telnetPassword",
|
||||
]),
|
||||
ssh_credentials: new Set([
|
||||
"password",
|
||||
|
||||
@@ -267,6 +267,10 @@ export class LazyFieldEncryption {
|
||||
autostartPassword: "autostart_password",
|
||||
autostartKey: "autostart_key",
|
||||
autostartKeyPassword: "autostart_key_password",
|
||||
socks5Password: "socks5_password",
|
||||
rdpPassword: "rdp_password",
|
||||
vncPassword: "vnc_password",
|
||||
telnetPassword: "telnet_password",
|
||||
totpSecret: "totp_secret",
|
||||
totpBackupCodes: "totp_backup_codes",
|
||||
clientSecret: "client_secret",
|
||||
|
||||
@@ -1,492 +0,0 @@
|
||||
import { getDb } from "../database/db/index.js";
|
||||
import {
|
||||
users,
|
||||
hosts,
|
||||
sshCredentials,
|
||||
fileManagerRecent,
|
||||
fileManagerPinned,
|
||||
fileManagerShortcuts,
|
||||
dismissedAlerts,
|
||||
} from "../database/db/schema.js";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { DataCrypto } from "./data-crypto.js";
|
||||
import { UserDataExport, type UserExportData } from "./user-data-export.js";
|
||||
import { databaseLogger } from "./logger.js";
|
||||
|
||||
interface ImportOptions {
|
||||
replaceExisting?: boolean;
|
||||
skipCredentials?: boolean;
|
||||
skipFileManagerData?: boolean;
|
||||
dryRun?: boolean;
|
||||
}
|
||||
|
||||
interface ImportResult {
|
||||
success: boolean;
|
||||
summary: {
|
||||
sshHostsImported: number;
|
||||
sshCredentialsImported: number;
|
||||
fileManagerItemsImported: number;
|
||||
dismissedAlertsImported: number;
|
||||
skippedItems: number;
|
||||
errors: string[];
|
||||
};
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
class UserDataImport {
|
||||
static async importUserData(
|
||||
targetUserId: string,
|
||||
exportData: UserExportData,
|
||||
options: ImportOptions = {},
|
||||
): Promise<ImportResult> {
|
||||
const {
|
||||
replaceExisting = false,
|
||||
skipCredentials = false,
|
||||
skipFileManagerData = false,
|
||||
dryRun = false,
|
||||
} = options;
|
||||
|
||||
try {
|
||||
const targetUser = await getDb()
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, targetUserId));
|
||||
if (!targetUser || targetUser.length === 0) {
|
||||
throw new Error(`Target user not found: ${targetUserId}`);
|
||||
}
|
||||
|
||||
const validation = UserDataExport.validateExportData(exportData);
|
||||
if (!validation.valid) {
|
||||
throw new Error(`Invalid export data: ${validation.errors.join(", ")}`);
|
||||
}
|
||||
|
||||
let userDataKey: Buffer | null = null;
|
||||
if (exportData.metadata.encrypted) {
|
||||
userDataKey = DataCrypto.getUserDataKey(targetUserId);
|
||||
if (!userDataKey) {
|
||||
throw new Error(
|
||||
"Target user data not unlocked - password required for encrypted import",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const result: ImportResult = {
|
||||
success: false,
|
||||
summary: {
|
||||
sshHostsImported: 0,
|
||||
sshCredentialsImported: 0,
|
||||
fileManagerItemsImported: 0,
|
||||
dismissedAlertsImported: 0,
|
||||
skippedItems: 0,
|
||||
errors: [],
|
||||
},
|
||||
dryRun,
|
||||
};
|
||||
|
||||
if (
|
||||
exportData.userData.sshHosts &&
|
||||
exportData.userData.sshHosts.length > 0
|
||||
) {
|
||||
const importStats = await this.importSshHosts(
|
||||
targetUserId,
|
||||
exportData.userData.sshHosts as Record<string, unknown>[],
|
||||
{ replaceExisting, dryRun, userDataKey },
|
||||
);
|
||||
result.summary.sshHostsImported = importStats.imported;
|
||||
result.summary.skippedItems += importStats.skipped;
|
||||
result.summary.errors.push(...importStats.errors);
|
||||
}
|
||||
|
||||
if (
|
||||
!skipCredentials &&
|
||||
exportData.userData.sshCredentials &&
|
||||
exportData.userData.sshCredentials.length > 0
|
||||
) {
|
||||
const importStats = await this.importSshCredentials(
|
||||
targetUserId,
|
||||
exportData.userData.sshCredentials as Record<string, unknown>[],
|
||||
{ replaceExisting, dryRun, userDataKey },
|
||||
);
|
||||
result.summary.sshCredentialsImported = importStats.imported;
|
||||
result.summary.skippedItems += importStats.skipped;
|
||||
result.summary.errors.push(...importStats.errors);
|
||||
}
|
||||
|
||||
if (!skipFileManagerData && exportData.userData.fileManagerData) {
|
||||
const importStats = await this.importFileManagerData(
|
||||
targetUserId,
|
||||
exportData.userData.fileManagerData,
|
||||
{ replaceExisting, dryRun },
|
||||
);
|
||||
result.summary.fileManagerItemsImported = importStats.imported;
|
||||
result.summary.skippedItems += importStats.skipped;
|
||||
result.summary.errors.push(...importStats.errors);
|
||||
}
|
||||
|
||||
if (
|
||||
exportData.userData.dismissedAlerts &&
|
||||
exportData.userData.dismissedAlerts.length > 0
|
||||
) {
|
||||
const importStats = await this.importDismissedAlerts(
|
||||
targetUserId,
|
||||
exportData.userData.dismissedAlerts as Record<string, unknown>[],
|
||||
{ replaceExisting, dryRun },
|
||||
);
|
||||
result.summary.dismissedAlertsImported = importStats.imported;
|
||||
result.summary.skippedItems += importStats.skipped;
|
||||
result.summary.errors.push(...importStats.errors);
|
||||
}
|
||||
|
||||
result.success = result.summary.errors.length === 0;
|
||||
|
||||
databaseLogger.success("User data import completed", {
|
||||
operation: "user_data_import_complete",
|
||||
targetUserId,
|
||||
dryRun,
|
||||
...result.summary,
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
databaseLogger.error("User data import failed", error, {
|
||||
operation: "user_data_import_failed",
|
||||
targetUserId,
|
||||
dryRun,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private static async importSshHosts(
|
||||
targetUserId: string,
|
||||
sshHosts: Record<string, unknown>[],
|
||||
options: {
|
||||
replaceExisting: boolean;
|
||||
dryRun: boolean;
|
||||
userDataKey: Buffer | null;
|
||||
},
|
||||
) {
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const host of sshHosts) {
|
||||
try {
|
||||
if (options.dryRun) {
|
||||
imported++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const existing = await getDb()
|
||||
.select()
|
||||
.from(hosts)
|
||||
.where(
|
||||
and(
|
||||
eq(hosts.userId, targetUserId),
|
||||
eq(hosts.ip, host.ip as string),
|
||||
eq(hosts.port, host.port as number),
|
||||
eq(hosts.username, host.username as string),
|
||||
),
|
||||
);
|
||||
|
||||
if (existing.length > 0 && !options.replaceExisting) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const newHostData: Record<string, unknown> = {
|
||||
...host,
|
||||
userId: targetUserId,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (existing.length === 0) {
|
||||
newHostData.createdAt = new Date().toISOString();
|
||||
}
|
||||
|
||||
let processedHostData: Record<string, unknown> = newHostData;
|
||||
if (options.userDataKey) {
|
||||
processedHostData = DataCrypto.encryptRecord(
|
||||
"ssh_data",
|
||||
newHostData,
|
||||
targetUserId,
|
||||
options.userDataKey,
|
||||
) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
delete processedHostData.id;
|
||||
|
||||
if (existing.length > 0 && options.replaceExisting) {
|
||||
await getDb()
|
||||
.update(hosts)
|
||||
.set(processedHostData as unknown as typeof hosts.$inferInsert)
|
||||
.where(eq(hosts.id, existing[0].id));
|
||||
} else {
|
||||
await getDb()
|
||||
.insert(hosts)
|
||||
.values(processedHostData as unknown as typeof hosts.$inferInsert);
|
||||
}
|
||||
imported++;
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`SSH host import failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
|
||||
return { imported, skipped, errors };
|
||||
}
|
||||
|
||||
private static async importSshCredentials(
|
||||
targetUserId: string,
|
||||
credentials: Record<string, unknown>[],
|
||||
options: {
|
||||
replaceExisting: boolean;
|
||||
dryRun: boolean;
|
||||
userDataKey: Buffer | null;
|
||||
},
|
||||
) {
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const credential of credentials) {
|
||||
try {
|
||||
if (options.dryRun) {
|
||||
imported++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const existing = await getDb()
|
||||
.select()
|
||||
.from(sshCredentials)
|
||||
.where(
|
||||
and(
|
||||
eq(sshCredentials.userId, targetUserId),
|
||||
eq(sshCredentials.name, credential.name as string),
|
||||
),
|
||||
);
|
||||
|
||||
if (existing.length > 0 && !options.replaceExisting) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const newCredentialData: Record<string, unknown> = {
|
||||
...credential,
|
||||
userId: targetUserId,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (existing.length === 0) {
|
||||
newCredentialData.usageCount = 0;
|
||||
newCredentialData.lastUsed = null;
|
||||
newCredentialData.createdAt = new Date().toISOString();
|
||||
}
|
||||
|
||||
let processedCredentialData: Record<string, unknown> =
|
||||
newCredentialData;
|
||||
if (options.userDataKey) {
|
||||
processedCredentialData = DataCrypto.encryptRecord(
|
||||
"ssh_credentials",
|
||||
newCredentialData,
|
||||
targetUserId,
|
||||
options.userDataKey,
|
||||
) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
delete processedCredentialData.id;
|
||||
|
||||
if (existing.length > 0 && options.replaceExisting) {
|
||||
await getDb()
|
||||
.update(sshCredentials)
|
||||
.set(
|
||||
processedCredentialData as unknown as typeof sshCredentials.$inferInsert,
|
||||
)
|
||||
.where(eq(sshCredentials.id, existing[0].id));
|
||||
} else {
|
||||
await getDb()
|
||||
.insert(sshCredentials)
|
||||
.values(
|
||||
processedCredentialData as unknown as typeof sshCredentials.$inferInsert,
|
||||
);
|
||||
}
|
||||
imported++;
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`SSH credential import failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
|
||||
return { imported, skipped, errors };
|
||||
}
|
||||
|
||||
private static async importFileManagerData(
|
||||
targetUserId: string,
|
||||
fileManagerData: Record<string, unknown>,
|
||||
options: { replaceExisting: boolean; dryRun: boolean },
|
||||
) {
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
try {
|
||||
if (fileManagerData.recent && Array.isArray(fileManagerData.recent)) {
|
||||
for (const item of fileManagerData.recent) {
|
||||
try {
|
||||
if (!options.dryRun) {
|
||||
const newItem = {
|
||||
...item,
|
||||
id: undefined,
|
||||
userId: targetUserId,
|
||||
lastOpened: new Date().toISOString(),
|
||||
};
|
||||
await getDb().insert(fileManagerRecent).values(newItem);
|
||||
}
|
||||
imported++;
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`Recent file import failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fileManagerData.pinned && Array.isArray(fileManagerData.pinned)) {
|
||||
for (const item of fileManagerData.pinned) {
|
||||
try {
|
||||
if (!options.dryRun) {
|
||||
const newItem = {
|
||||
...item,
|
||||
id: undefined,
|
||||
userId: targetUserId,
|
||||
pinnedAt: new Date().toISOString(),
|
||||
};
|
||||
await getDb().insert(fileManagerPinned).values(newItem);
|
||||
}
|
||||
imported++;
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`Pinned file import failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
fileManagerData.shortcuts &&
|
||||
Array.isArray(fileManagerData.shortcuts)
|
||||
) {
|
||||
for (const item of fileManagerData.shortcuts) {
|
||||
try {
|
||||
if (!options.dryRun) {
|
||||
const newItem = {
|
||||
...item,
|
||||
id: undefined,
|
||||
userId: targetUserId,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
await getDb().insert(fileManagerShortcuts).values(newItem);
|
||||
}
|
||||
imported++;
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`Shortcut import failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`File manager data import failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
|
||||
return { imported, skipped, errors };
|
||||
}
|
||||
|
||||
private static async importDismissedAlerts(
|
||||
targetUserId: string,
|
||||
alerts: Record<string, unknown>[],
|
||||
options: { replaceExisting: boolean; dryRun: boolean },
|
||||
) {
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const alert of alerts) {
|
||||
try {
|
||||
if (options.dryRun) {
|
||||
imported++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const existing = await getDb()
|
||||
.select()
|
||||
.from(dismissedAlerts)
|
||||
.where(
|
||||
and(
|
||||
eq(dismissedAlerts.userId, targetUserId),
|
||||
eq(dismissedAlerts.alertId, alert.alertId as string),
|
||||
),
|
||||
);
|
||||
|
||||
if (existing.length > 0 && !options.replaceExisting) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const newAlert = {
|
||||
...alert,
|
||||
id: undefined,
|
||||
userId: targetUserId,
|
||||
dismissedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (existing.length > 0 && options.replaceExisting) {
|
||||
await getDb()
|
||||
.update(dismissedAlerts)
|
||||
.set(newAlert as typeof dismissedAlerts.$inferInsert)
|
||||
.where(eq(dismissedAlerts.id, existing[0].id));
|
||||
} else {
|
||||
await getDb()
|
||||
.insert(dismissedAlerts)
|
||||
.values(newAlert as typeof dismissedAlerts.$inferInsert);
|
||||
}
|
||||
|
||||
imported++;
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`Dismissed alert import failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
|
||||
return { imported, skipped, errors };
|
||||
}
|
||||
|
||||
static async importUserDataFromJSON(
|
||||
targetUserId: string,
|
||||
jsonData: string,
|
||||
options: ImportOptions = {},
|
||||
): Promise<ImportResult> {
|
||||
try {
|
||||
const exportData: UserExportData = JSON.parse(jsonData);
|
||||
return await this.importUserData(targetUserId, exportData, options);
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
throw new Error("Invalid JSON format in import data", { cause: error });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { UserDataImport, type ImportOptions, type ImportResult };
|
||||
Reference in New Issue
Block a user