mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-16 05:13:36 +00:00
e9e30cd318
* Guacd, Docker-Compose, RDP (#475) * fix select edit host but not update view (#438) * fix: Checksum issue with chocolatey * fix: Remove homebrew old stuff * Add Korean translation (#439) Co-authored-by: 송준우 <2484@coreit.co.kr> * feat: Automate flatpak * fix: Add imagemagik to electron builder to resolve build error * fix: Build error with runtime repo flag * fix: Flatpak runtime error and install freedesktop ver warning * fix: Flatpak runtime error and install freedesktop ver warning * feat: Re-add homebrew cask and move scripts to backend * fix: No sandbox flag issue * fix: Change name for electron macos cask output * fix: Sandbox error with Linux * fix: Remove comming soon for app stores in readme * Adding Comment at the end of the public_key on the host on deploy (#440) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * -Add New Interface for Credential DB -Add Credential Name as a comment into the server authorized_key file --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Sudo auto fill password (#441) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Feature Sudo password auto-fill; * Fix locale json shema; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Added Italian Language; (#445) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Added Italian Language; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Auto collapse snippet folders (#448) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * feat: Add collapsable snippets (customizable in user profile) * Translations (#447) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Added Italian Language; * Fix translations; Removed duplicate keys, synchronised other languages using English as the source, translated added keys, fixed inaccurate translations. --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Remove PTY-level keepalive (#449) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Remove PTY-level keepalive to prevent unwanted terminal output; use SSH-level keepalive instead --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add Guacamole support for RDP, VNC, and Telnet connections - Implemented WebSocket support for Guacamole in Nginx configuration. - Added REST API endpoints for generating connection tokens and checking guacd status. - Created Guacamole server using guacamole-lite for handling connections. - Developed frontend components for testing RDP/VNC connections and displaying the remote session. - Updated package dependencies to include guacamole-common-js and guacamole-lite. - Enhanced logging for Guacamole operations. * feat: enhance Guacamole support with RDP and VNC connection settings and UI updates * feat: Seperate server stats and tunnel management (improved both UI's) then started initial docker implementation * fix: finalize adding docker to db * fix: merge syntax errors * feat: implement mouse coordinate adjustment based on scale factor in GuacamoleDisplay * feat: add TypeScript definitions for guacamole-common-js module * feat: enhance Mouse.State constructor to accept optional parameters and object destructuring * feat: Add support for RDP and VNC connections in SSH host management - Introduced connectionType field to differentiate between SSH, RDP, VNC, and Telnet in host data structures. - Updated backend routes to handle RDP/VNC specific fields: domain, security, and ignoreCert. - Enhanced the HostManagerEditor to include RDP/VNC specific settings and authentication options. - Implemented token retrieval for RDP/VNC connections using Guacamole API. - Updated UI components to reflect connection type changes and provide appropriate connection buttons. - Removed the GuacamoleTestDialog component as its functionality is integrated into the HostManagerEditor. - Adjusted the TopNavbar and Host components to accommodate new connection types and their respective actions. * feat: Enhance Guacamole integration with extended configuration options - Added detailed Guacamole configuration interface for RDP/VNC/Telnet connections, including display, audio, performance, and session settings. - Implemented logging for token requests and received options for better debugging. - Updated HostManagerEditor to support new Guacamole configuration fields with validation and default values. - Integrated Guacamole configuration parsing in HostManagerViewer and Host components. - Enhanced API requests to include extended Guacamole configuration parameters in the token request. - Refactored code to convert camelCase configuration keys to kebab-case for compatibility with Guacamole API. * feat: merge guacd into 2.0.0 and improve UI for host manager and made general bug fixes --------- Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: junu <bigdwarf_@naver.com> Co-authored-by: 송준우 <2484@coreit.co.kr> Co-authored-by: SlimGary <trash.slim@gmail.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: rename api routes and files * feat: improve guacd ui/backend * feat: improve guacd ui/backend * fix: state persistance issues causing refresh * feat: improge guacd connections, fixed telnet not opening, and improved general guacd integration * feat: continue improving integration also with bug fixes * Merge 2.0.0 with 2.0.0 that includes bug fixes (#620) * Guacd, Docker-Compose, RDP (#475) * fix select edit host but not update view (#438) * fix: Checksum issue with chocolatey * fix: Remove homebrew old stuff * Add Korean translation (#439) Co-authored-by: 송준우 <2484@coreit.co.kr> * feat: Automate flatpak * fix: Add imagemagik to electron builder to resolve build error * fix: Build error with runtime repo flag * fix: Flatpak runtime error and install freedesktop ver warning * fix: Flatpak runtime error and install freedesktop ver warning * feat: Re-add homebrew cask and move scripts to backend * fix: No sandbox flag issue * fix: Change name for electron macos cask output * fix: Sandbox error with Linux * fix: Remove comming soon for app stores in readme * Adding Comment at the end of the public_key on the host on deploy (#440) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * -Add New Interface for Credential DB -Add Credential Name as a comment into the server authorized_key file --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Sudo auto fill password (#441) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Feature Sudo password auto-fill; * Fix locale json shema; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Added Italian Language; (#445) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Added Italian Language; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Auto collapse snippet folders (#448) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * feat: Add collapsable snippets (customizable in user profile) * Translations (#447) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Added Italian Language; * Fix translations; Removed duplicate keys, synchronised other languages using English as the source, translated added keys, fixed inaccurate translations. --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Remove PTY-level keepalive (#449) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Remove PTY-level keepalive to prevent unwanted terminal output; use SSH-level keepalive instead --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add Guacamole support for RDP, VNC, and Telnet connections - Implemented WebSocket support for Guacamole in Nginx configuration. - Added REST API endpoints for generating connection tokens and checking guacd status. - Created Guacamole server using guacamole-lite for handling connections. - Developed frontend components for testing RDP/VNC connections and displaying the remote session. - Updated package dependencies to include guacamole-common-js and guacamole-lite. - Enhanced logging for Guacamole operations. * feat: enhance Guacamole support with RDP and VNC connection settings and UI updates * feat: Seperate server stats and tunnel management (improved both UI's) then started initial docker implementation * fix: finalize adding docker to db * fix: merge syntax errors * feat: implement mouse coordinate adjustment based on scale factor in GuacamoleDisplay * feat: add TypeScript definitions for guacamole-common-js module * feat: enhance Mouse.State constructor to accept optional parameters and object destructuring * feat: Add support for RDP and VNC connections in SSH host management - Introduced connectionType field to differentiate between SSH, RDP, VNC, and Telnet in host data structures. - Updated backend routes to handle RDP/VNC specific fields: domain, security, and ignoreCert. - Enhanced the HostManagerEditor to include RDP/VNC specific settings and authentication options. - Implemented token retrieval for RDP/VNC connections using Guacamole API. - Updated UI components to reflect connection type changes and provide appropriate connection buttons. - Removed the GuacamoleTestDialog component as its functionality is integrated into the HostManagerEditor. - Adjusted the TopNavbar and Host components to accommodate new connection types and their respective actions. * feat: Enhance Guacamole integration with extended configuration options - Added detailed Guacamole configuration interface for RDP/VNC/Telnet connections, including display, audio, performance, and session settings. - Implemented logging for token requests and received options for better debugging. - Updated HostManagerEditor to support new Guacamole configuration fields with validation and default values. - Integrated Guacamole configuration parsing in HostManagerViewer and Host components. - Enhanced API requests to include extended Guacamole configuration parameters in the token request. - Refactored code to convert camelCase configuration keys to kebab-case for compatibility with Guacamole API. * feat: merge guacd into 2.0.0 and improve UI for host manager and made general bug fixes --------- Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: junu <bigdwarf_@naver.com> Co-authored-by: 송준우 <2484@coreit.co.kr> Co-authored-by: SlimGary <trash.slim@gmail.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: rename api routes and files * feat: improve guacd ui/backend * feat: improve guacd ui/backend * fix: state persistance issues causing refresh * feat: improge guacd connections, fixed telnet not opening, and improved general guacd integration * feat: continue improving integration also with bug fixes --------- Co-authored-by: Wesley Reid <starhound@lostsouls.org> Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com> Co-authored-by: junu <bigdwarf_@naver.com> Co-authored-by: 송준우 <2484@coreit.co.kr> Co-authored-by: SlimGary <trash.slim@gmail.com> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: allow customizing guacd backened url * fix: ssh route mistmatching and guacamole url not changing * chore: increment ver * feat: change default to work with default compose, added splits creen support, updated readmes * fix: linux app not starting due to better sqlite isuses, improved copy/paste system so no context menu, added oidc remember me toggle, improved OS detection for sessions, flatpak invalid key, and sharing hosts with other users errors * fix: global settings not setting * chore: update compose * feat: improve the global status input * chore: cleanup files * chore: update export/improt with new host fields * fix: file manager and docker not loading properly --------- Co-authored-by: Wesley Reid <starhound@lostsouls.org> Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com> Co-authored-by: junu <bigdwarf_@naver.com> Co-authored-by: 송준우 <2484@coreit.co.kr> Co-authored-by: SlimGary <trash.slim@gmail.com> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
493 lines
14 KiB
TypeScript
493 lines
14 KiB
TypeScript
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");
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
export { UserDataImport, type ImportOptions, type ImportResult };
|