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:
@@ -1044,6 +1044,23 @@ const migrateSchema = () => {
|
||||
{ column: "host_key_changed_count", sql: "ALTER TABLE ssh_data ADD COLUMN host_key_changed_count INTEGER NOT NULL DEFAULT 0" },
|
||||
{ column: "mac_address", sql: "ALTER TABLE ssh_data ADD COLUMN mac_address TEXT" },
|
||||
{ column: "port_knock_sequence", sql: "ALTER TABLE ssh_data ADD COLUMN port_knock_sequence TEXT" },
|
||||
{ column: "enable_ssh", sql: "ALTER TABLE ssh_data ADD COLUMN enable_ssh INTEGER NOT NULL DEFAULT 1" },
|
||||
{ column: "enable_rdp", sql: "ALTER TABLE ssh_data ADD COLUMN enable_rdp INTEGER NOT NULL DEFAULT 0" },
|
||||
{ column: "enable_vnc", sql: "ALTER TABLE ssh_data ADD COLUMN enable_vnc INTEGER NOT NULL DEFAULT 0" },
|
||||
{ column: "enable_telnet", sql: "ALTER TABLE ssh_data ADD COLUMN enable_telnet INTEGER NOT NULL DEFAULT 0" },
|
||||
{ column: "ssh_port", sql: "ALTER TABLE ssh_data ADD COLUMN ssh_port INTEGER DEFAULT 22" },
|
||||
{ column: "rdp_port", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_port INTEGER DEFAULT 3389" },
|
||||
{ column: "vnc_port", sql: "ALTER TABLE ssh_data ADD COLUMN vnc_port INTEGER DEFAULT 5900" },
|
||||
{ column: "telnet_port", sql: "ALTER TABLE ssh_data ADD COLUMN telnet_port INTEGER DEFAULT 23" },
|
||||
{ column: "rdp_user", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_user TEXT" },
|
||||
{ column: "rdp_password", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_password TEXT" },
|
||||
{ column: "rdp_domain", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_domain TEXT" },
|
||||
{ column: "rdp_security", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_security TEXT" },
|
||||
{ column: "rdp_ignore_cert", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_ignore_cert INTEGER DEFAULT 0" },
|
||||
{ column: "vnc_password", sql: "ALTER TABLE ssh_data ADD COLUMN vnc_password TEXT" },
|
||||
{ column: "vnc_user", sql: "ALTER TABLE ssh_data ADD COLUMN vnc_user TEXT" },
|
||||
{ column: "telnet_user", sql: "ALTER TABLE ssh_data ADD COLUMN telnet_user TEXT" },
|
||||
{ column: "telnet_password", sql: "ALTER TABLE ssh_data ADD COLUMN telnet_password TEXT" },
|
||||
];
|
||||
|
||||
for (const migration of sshDataMigrations) {
|
||||
|
||||
@@ -129,6 +129,28 @@ export const hosts = sqliteTable("ssh_data", {
|
||||
terminalConfig: text("terminal_config"),
|
||||
quickActions: text("quick_actions"),
|
||||
notes: text("notes"),
|
||||
enableSsh: integer("enable_ssh", { mode: "boolean" }).notNull().default(true),
|
||||
enableRdp: integer("enable_rdp", { mode: "boolean" }).notNull().default(false),
|
||||
enableVnc: integer("enable_vnc", { mode: "boolean" }).notNull().default(false),
|
||||
enableTelnet: integer("enable_telnet", { mode: "boolean" }).notNull().default(false),
|
||||
|
||||
sshPort: integer("ssh_port").default(22),
|
||||
rdpPort: integer("rdp_port").default(3389),
|
||||
vncPort: integer("vnc_port").default(5900),
|
||||
telnetPort: integer("telnet_port").default(23),
|
||||
|
||||
rdpUser: text("rdp_user"),
|
||||
rdpPassword: text("rdp_password"),
|
||||
rdpDomain: text("rdp_domain"),
|
||||
rdpSecurity: text("rdp_security"),
|
||||
rdpIgnoreCert: integer("rdp_ignore_cert", { mode: "boolean" }).default(false),
|
||||
|
||||
vncPassword: text("vnc_password"),
|
||||
vncUser: text("vnc_user"),
|
||||
|
||||
telnetUser: text("telnet_user"),
|
||||
telnetPassword: text("telnet_password"),
|
||||
|
||||
domain: text("domain"),
|
||||
security: text("security"),
|
||||
ignoreCert: integer("ignore_cert", { mode: "boolean" }).default(false),
|
||||
|
||||
@@ -252,6 +252,9 @@ const SENSITIVE_FIELDS = [
|
||||
"autostartKey",
|
||||
"autostartKeyPassword",
|
||||
"socks5Password",
|
||||
"rdpPassword",
|
||||
"vncPassword",
|
||||
"telnetPassword",
|
||||
];
|
||||
|
||||
function stripSensitiveFields(
|
||||
@@ -288,6 +291,23 @@ function transformHostResponse(
|
||||
showTunnelInSidebar: !!host.showTunnelInSidebar,
|
||||
showDockerInSidebar: !!host.showDockerInSidebar,
|
||||
showServerStatsInSidebar: !!host.showServerStatsInSidebar,
|
||||
enableSsh: host.enableSsh !== undefined ? !!host.enableSsh : true,
|
||||
enableRdp: !!host.enableRdp,
|
||||
enableVnc: !!host.enableVnc,
|
||||
enableTelnet: !!host.enableTelnet,
|
||||
sshPort: host.sshPort ?? host.port ?? 22,
|
||||
rdpPort: host.rdpPort ?? 3389,
|
||||
vncPort: host.vncPort ?? 5900,
|
||||
telnetPort: host.telnetPort ?? 23,
|
||||
rdpUser: host.rdpUser || undefined,
|
||||
rdpDomain: host.rdpDomain || undefined,
|
||||
rdpSecurity: host.rdpSecurity || undefined,
|
||||
rdpIgnoreCert: !!host.rdpIgnoreCert,
|
||||
vncUser: host.vncUser || undefined,
|
||||
telnetUser: host.telnetUser || undefined,
|
||||
hasRdpPassword: !!host.rdpPassword,
|
||||
hasVncPassword: !!host.vncPassword,
|
||||
hasTelnetPassword: !!host.telnetPassword,
|
||||
tunnelConnections: host.tunnelConnections
|
||||
? JSON.parse(host.tunnelConnections as string)
|
||||
: [],
|
||||
@@ -593,6 +613,23 @@ router.post(
|
||||
portKnockSequence,
|
||||
overrideCredentialUsername,
|
||||
macAddress,
|
||||
enableSsh,
|
||||
enableRdp,
|
||||
enableVnc,
|
||||
enableTelnet,
|
||||
sshPort,
|
||||
rdpPort,
|
||||
vncPort,
|
||||
telnetPort,
|
||||
rdpUser,
|
||||
rdpPassword,
|
||||
rdpDomain,
|
||||
rdpSecurity,
|
||||
rdpIgnoreCert,
|
||||
vncPassword,
|
||||
vncUser,
|
||||
telnetUser,
|
||||
telnetPassword,
|
||||
} = hostData;
|
||||
databaseLogger.info("Creating SSH host", {
|
||||
operation: "host_create",
|
||||
@@ -685,6 +722,20 @@ router.post(
|
||||
portKnockSequence: portKnockSequence
|
||||
? JSON.stringify(portKnockSequence)
|
||||
: null,
|
||||
enableSsh: enableSsh !== false ? 1 : 0,
|
||||
enableRdp: enableRdp ? 1 : 0,
|
||||
enableVnc: enableVnc ? 1 : 0,
|
||||
enableTelnet: enableTelnet ? 1 : 0,
|
||||
sshPort: sshPort || port || 22,
|
||||
rdpPort: rdpPort || 3389,
|
||||
vncPort: vncPort || 5900,
|
||||
telnetPort: telnetPort || 23,
|
||||
rdpUser: rdpUser || null,
|
||||
rdpDomain: rdpDomain || null,
|
||||
rdpSecurity: rdpSecurity || null,
|
||||
rdpIgnoreCert: rdpIgnoreCert ? 1 : 0,
|
||||
vncUser: vncUser || null,
|
||||
telnetUser: telnetUser || null,
|
||||
};
|
||||
|
||||
// For non-SSH hosts (RDP, VNC, Telnet), always save password if provided
|
||||
@@ -743,6 +794,10 @@ router.post(
|
||||
sshDataObj.keyType = null;
|
||||
}
|
||||
|
||||
sshDataObj.rdpPassword = rdpPassword || null;
|
||||
sshDataObj.vncPassword = vncPassword || null;
|
||||
sshDataObj.telnetPassword = telnetPassword || null;
|
||||
|
||||
try {
|
||||
const result = await SimpleDBOps.insert(
|
||||
hosts,
|
||||
@@ -1092,6 +1147,23 @@ router.put(
|
||||
portKnockSequence,
|
||||
overrideCredentialUsername,
|
||||
macAddress,
|
||||
enableSsh,
|
||||
enableRdp,
|
||||
enableVnc,
|
||||
enableTelnet,
|
||||
sshPort,
|
||||
rdpPort,
|
||||
vncPort,
|
||||
telnetPort,
|
||||
rdpUser,
|
||||
rdpPassword,
|
||||
rdpDomain,
|
||||
rdpSecurity,
|
||||
rdpIgnoreCert,
|
||||
vncPassword,
|
||||
vncUser,
|
||||
telnetUser,
|
||||
telnetPassword,
|
||||
} = hostData;
|
||||
databaseLogger.info("Updating SSH host", {
|
||||
operation: "host_update",
|
||||
@@ -1181,6 +1253,20 @@ router.put(
|
||||
portKnockSequence: portKnockSequence
|
||||
? JSON.stringify(portKnockSequence)
|
||||
: null,
|
||||
enableSsh: enableSsh !== false ? 1 : 0,
|
||||
enableRdp: enableRdp ? 1 : 0,
|
||||
enableVnc: enableVnc ? 1 : 0,
|
||||
enableTelnet: enableTelnet ? 1 : 0,
|
||||
sshPort: sshPort || port || 22,
|
||||
rdpPort: rdpPort || 3389,
|
||||
vncPort: vncPort || 5900,
|
||||
telnetPort: telnetPort || 23,
|
||||
rdpUser: rdpUser || null,
|
||||
rdpDomain: rdpDomain || null,
|
||||
rdpSecurity: rdpSecurity || null,
|
||||
rdpIgnoreCert: rdpIgnoreCert ? 1 : 0,
|
||||
vncUser: vncUser || null,
|
||||
telnetUser: telnetUser || null,
|
||||
};
|
||||
|
||||
// For non-SSH hosts (RDP, VNC, Telnet), always save password if provided
|
||||
@@ -1249,6 +1335,11 @@ router.put(
|
||||
sshDataObj.keyType = null;
|
||||
}
|
||||
|
||||
if (rdpPassword !== undefined) sshDataObj.rdpPassword = rdpPassword || null;
|
||||
if (vncPassword !== undefined) sshDataObj.vncPassword = vncPassword || null;
|
||||
if (telnetPassword !== undefined)
|
||||
sshDataObj.telnetPassword = telnetPassword || null;
|
||||
|
||||
try {
|
||||
const accessInfo = await permissionManager.canAccessHost(
|
||||
userId,
|
||||
|
||||
@@ -676,7 +676,18 @@ interface StatsConfig {
|
||||
}
|
||||
|
||||
const DEFAULT_STATS_CONFIG: StatsConfig = {
|
||||
enabledWidgets: ["cpu", "memory", "disk", "network", "uptime", "system"],
|
||||
enabledWidgets: [
|
||||
"cpu",
|
||||
"memory",
|
||||
"disk",
|
||||
"network",
|
||||
"uptime",
|
||||
"system",
|
||||
"login_stats",
|
||||
"processes",
|
||||
"ports",
|
||||
"firewall",
|
||||
],
|
||||
statusCheckEnabled: true,
|
||||
statusCheckInterval: 60,
|
||||
metricsEnabled: true,
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
import swaggerJSDoc from "@deadendjs/swagger-jsdoc";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { promises as fs } from "fs";
|
||||
import { systemLogger } from "./utils/logger.js";
|
||||
|
||||
interface SwaggerOptions {
|
||||
definition: object;
|
||||
apis: string[];
|
||||
}
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const projectRoot = path.join(__dirname, "..", "..", "..");
|
||||
|
||||
const swaggerOptions: SwaggerOptions = {
|
||||
definition: {
|
||||
openapi: "3.0.3",
|
||||
info: {
|
||||
title: "Termix API",
|
||||
version: "0.0.0",
|
||||
description: "Termix Backend API Reference",
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: "http://localhost:30001",
|
||||
description: "Main database and authentication server",
|
||||
},
|
||||
{
|
||||
url: "http://localhost:30003",
|
||||
description: "SSH tunnel management server",
|
||||
},
|
||||
{
|
||||
url: "http://localhost:30004",
|
||||
description: "SSH file manager server",
|
||||
},
|
||||
{
|
||||
url: "http://localhost:30005",
|
||||
description: "Server statistics and monitoring server",
|
||||
},
|
||||
{
|
||||
url: "http://localhost:30006",
|
||||
description: "Dashboard server",
|
||||
},
|
||||
{
|
||||
url: "http://localhost:30007",
|
||||
description: "Docker management server",
|
||||
},
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: "http",
|
||||
scheme: "bearer",
|
||||
bearerFormat: "JWT",
|
||||
},
|
||||
},
|
||||
schemas: {
|
||||
Error: {
|
||||
type: "object",
|
||||
properties: {
|
||||
error: { type: "string" },
|
||||
details: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
security: [
|
||||
{
|
||||
bearerAuth: [],
|
||||
},
|
||||
],
|
||||
tags: [
|
||||
{
|
||||
name: "Alerts",
|
||||
description: "System alerts and notifications management",
|
||||
},
|
||||
{
|
||||
name: "Credentials",
|
||||
description: "SSH credential management",
|
||||
},
|
||||
{
|
||||
name: "Network Topology",
|
||||
description: "Network topology visualization and management",
|
||||
},
|
||||
{
|
||||
name: "RBAC",
|
||||
description: "Role-based access control for host sharing",
|
||||
},
|
||||
{
|
||||
name: "Snippets",
|
||||
description: "Command snippet management",
|
||||
},
|
||||
{
|
||||
name: "Terminal",
|
||||
description: "Terminal command history",
|
||||
},
|
||||
{
|
||||
name: "Users",
|
||||
description: "User management and authentication",
|
||||
},
|
||||
{
|
||||
name: "Dashboard",
|
||||
description: "Dashboard statistics and activity",
|
||||
},
|
||||
{
|
||||
name: "Docker",
|
||||
description: "Docker container management",
|
||||
},
|
||||
{
|
||||
name: "SSH Tunnels",
|
||||
description: "SSH tunnel connection management",
|
||||
},
|
||||
{
|
||||
name: "Server Stats",
|
||||
description: "Server status monitoring and metrics collection",
|
||||
},
|
||||
{
|
||||
name: "File Manager",
|
||||
description: "SSH file management operations",
|
||||
},
|
||||
],
|
||||
},
|
||||
apis: [
|
||||
path.join(projectRoot, "src", "backend", "database", "routes", "*.ts"),
|
||||
path.join(projectRoot, "src", "backend", "dashboard.ts"),
|
||||
path.join(projectRoot, "src", "backend", "ssh", "*.ts"),
|
||||
],
|
||||
};
|
||||
|
||||
async function generateOpenAPISpec() {
|
||||
try {
|
||||
systemLogger.info("Generating OpenAPI specification", {
|
||||
operation: "openapi_generate_start",
|
||||
});
|
||||
|
||||
const swaggerSpec = await swaggerJSDoc(swaggerOptions);
|
||||
|
||||
const outputPath = path.join(projectRoot, "openapi.json");
|
||||
|
||||
await fs.writeFile(
|
||||
outputPath,
|
||||
JSON.stringify(swaggerSpec, null, 2),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
systemLogger.success("OpenAPI specification generated", {
|
||||
operation: "openapi_generate_success",
|
||||
});
|
||||
} catch (error) {
|
||||
systemLogger.error("Failed to generate OpenAPI specification", error, {
|
||||
operation: "openapi_generation",
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
generateOpenAPISpec();
|
||||
|
||||
export { swaggerOptions, generateOpenAPISpec };
|
||||
@@ -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