mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
feat: add domain settings support in backup import and export processes
This commit is contained in:
@@ -51,6 +51,7 @@ export interface BackupPayload {
|
|||||||
db: {
|
db: {
|
||||||
config: SqlRow[];
|
config: SqlRow[];
|
||||||
users: SqlRow[];
|
users: SqlRow[];
|
||||||
|
domain_settings: SqlRow[];
|
||||||
user_revisions: SqlRow[];
|
user_revisions: SqlRow[];
|
||||||
folders: SqlRow[];
|
folders: SqlRow[];
|
||||||
ciphers: SqlRow[];
|
ciphers: SqlRow[];
|
||||||
@@ -284,6 +285,7 @@ export function validateBackupPayloadContents(
|
|||||||
const configRows = ensureRowArray(payload.db.config, 'config');
|
const configRows = ensureRowArray(payload.db.config, 'config');
|
||||||
const userRows = ensureRowArray(payload.db.users, 'users');
|
const userRows = ensureRowArray(payload.db.users, 'users');
|
||||||
const revisionRows = ensureRowArray(payload.db.user_revisions, 'user_revisions');
|
const revisionRows = ensureRowArray(payload.db.user_revisions, 'user_revisions');
|
||||||
|
const domainSettingsRows = ensureRowArray(payload.db.domain_settings || [], 'domain_settings');
|
||||||
const folderRows = ensureRowArray(payload.db.folders, 'folders');
|
const folderRows = ensureRowArray(payload.db.folders, 'folders');
|
||||||
const cipherRows = ensureRowArray(payload.db.ciphers, 'ciphers');
|
const cipherRows = ensureRowArray(payload.db.ciphers, 'ciphers');
|
||||||
const attachmentRows = ensureRowArray(payload.db.attachments, 'attachments');
|
const attachmentRows = ensureRowArray(payload.db.attachments, 'attachments');
|
||||||
@@ -314,6 +316,18 @@ export function validateBackupPayloadContents(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const domainSettingUserIds = new Set<string>();
|
||||||
|
for (const row of domainSettingsRows) {
|
||||||
|
const userId = String(row.user_id || '').trim();
|
||||||
|
if (!userId || !userIds.has(userId)) {
|
||||||
|
throw new Error(`Backup archive contains domain settings for an unknown user: ${userId || '(empty)'}`);
|
||||||
|
}
|
||||||
|
if (domainSettingUserIds.has(userId)) {
|
||||||
|
throw new Error(`Backup archive contains duplicate domain settings for user: ${userId}`);
|
||||||
|
}
|
||||||
|
domainSettingUserIds.add(userId);
|
||||||
|
}
|
||||||
|
|
||||||
const folderIds = new Set<string>();
|
const folderIds = new Set<string>();
|
||||||
for (const row of folderRows) {
|
for (const row of folderRows) {
|
||||||
const id = String(row.id || '').trim();
|
const id = String(row.id || '').trim();
|
||||||
@@ -365,9 +379,10 @@ export async function buildBackupArchive(
|
|||||||
includeAttachments,
|
includeAttachments,
|
||||||
});
|
});
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const [configRows, userRows, revisionRows, folderRows, cipherRows, attachmentRows] = await Promise.all([
|
const [configRows, userRows, domainSettingsRows, revisionRows, folderRows, cipherRows, attachmentRows] = await Promise.all([
|
||||||
queryRows(env.DB, 'SELECT key, value FROM config ORDER BY key ASC'),
|
queryRows(env.DB, 'SELECT key, value FROM config ORDER BY key ASC'),
|
||||||
queryRows(env.DB, 'SELECT id, email, name, master_password_hint, master_password_hash, key, private_key, public_key, kdf_type, kdf_iterations, kdf_memory, kdf_parallelism, security_stamp, role, status, verify_devices, totp_secret, totp_recovery_code, api_key, created_at, updated_at FROM users ORDER BY created_at ASC'),
|
queryRows(env.DB, 'SELECT id, email, name, master_password_hint, master_password_hash, key, private_key, public_key, kdf_type, kdf_iterations, kdf_memory, kdf_parallelism, security_stamp, role, status, verify_devices, totp_secret, totp_recovery_code, created_at, updated_at FROM users ORDER BY created_at ASC'),
|
||||||
|
queryRows(env.DB, 'SELECT user_id, equivalent_domains, custom_equivalent_domains, excluded_global_equivalent_domains, updated_at FROM domain_settings ORDER BY user_id ASC'),
|
||||||
queryRows(env.DB, 'SELECT user_id, revision_date FROM user_revisions ORDER BY user_id ASC'),
|
queryRows(env.DB, 'SELECT user_id, revision_date FROM user_revisions ORDER BY user_id ASC'),
|
||||||
queryRows(env.DB, 'SELECT id, user_id, name, created_at, updated_at FROM folders ORDER BY created_at ASC'),
|
queryRows(env.DB, 'SELECT id, user_id, name, created_at, updated_at FROM folders ORDER BY created_at ASC'),
|
||||||
queryRows(env.DB, 'SELECT id, user_id, type, folder_id, name, notes, favorite, data, reprompt, key, created_at, updated_at, archived_at, deleted_at FROM ciphers ORDER BY created_at ASC'),
|
queryRows(env.DB, 'SELECT id, user_id, type, folder_id, name, notes, favorite, data, reprompt, key, created_at, updated_at, archived_at, deleted_at FROM ciphers ORDER BY created_at ASC'),
|
||||||
@@ -394,6 +409,7 @@ export async function buildBackupArchive(
|
|||||||
tableCounts: {
|
tableCounts: {
|
||||||
config: exportedConfigRows.length,
|
config: exportedConfigRows.length,
|
||||||
users: userRows.length,
|
users: userRows.length,
|
||||||
|
domain_settings: domainSettingsRows.length,
|
||||||
user_revisions: revisionRows.length,
|
user_revisions: revisionRows.length,
|
||||||
folders: folderRows.length,
|
folders: folderRows.length,
|
||||||
ciphers: cipherRows.length,
|
ciphers: cipherRows.length,
|
||||||
@@ -415,6 +431,7 @@ export async function buildBackupArchive(
|
|||||||
'db.json': encoder.encode(JSON.stringify({
|
'db.json': encoder.encode(JSON.stringify({
|
||||||
config: exportedConfigRows,
|
config: exportedConfigRows,
|
||||||
users: userRows,
|
users: userRows,
|
||||||
|
domain_settings: domainSettingsRows,
|
||||||
user_revisions: revisionRows,
|
user_revisions: revisionRows,
|
||||||
folders: folderRows,
|
folders: folderRows,
|
||||||
ciphers: cipherRows,
|
ciphers: cipherRows,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type SqlRow = Record<string, string | number | null>;
|
|||||||
type BackupTableName =
|
type BackupTableName =
|
||||||
| 'config'
|
| 'config'
|
||||||
| 'users'
|
| 'users'
|
||||||
|
| 'domain_settings'
|
||||||
| 'user_revisions'
|
| 'user_revisions'
|
||||||
| 'folders'
|
| 'folders'
|
||||||
| 'ciphers'
|
| 'ciphers'
|
||||||
@@ -20,6 +21,7 @@ type BackupTableName =
|
|||||||
const BACKUP_TABLES: BackupTableName[] = [
|
const BACKUP_TABLES: BackupTableName[] = [
|
||||||
'config',
|
'config',
|
||||||
'users',
|
'users',
|
||||||
|
'domain_settings',
|
||||||
'user_revisions',
|
'user_revisions',
|
||||||
'folders',
|
'folders',
|
||||||
'ciphers',
|
'ciphers',
|
||||||
@@ -35,6 +37,7 @@ export interface BackupImportResultBody {
|
|||||||
imported: {
|
imported: {
|
||||||
config: number;
|
config: number;
|
||||||
users: number;
|
users: number;
|
||||||
|
domainSettings: number;
|
||||||
userRevisions: number;
|
userRevisions: number;
|
||||||
folders: number;
|
folders: number;
|
||||||
ciphers: number;
|
ciphers: number;
|
||||||
@@ -155,6 +158,7 @@ function buildResetImportTargetStatements(db: D1Database): D1PreparedStatement[]
|
|||||||
'DELETE FROM attachments',
|
'DELETE FROM attachments',
|
||||||
'DELETE FROM ciphers',
|
'DELETE FROM ciphers',
|
||||||
'DELETE FROM folders',
|
'DELETE FROM folders',
|
||||||
|
'DELETE FROM domain_settings',
|
||||||
'DELETE FROM user_revisions',
|
'DELETE FROM user_revisions',
|
||||||
'DELETE FROM users',
|
'DELETE FROM users',
|
||||||
'DELETE FROM config',
|
'DELETE FROM config',
|
||||||
@@ -276,6 +280,7 @@ async function importPreparedBackupRows(db: D1Database, payload: BackupPayload['
|
|||||||
...row,
|
...row,
|
||||||
verify_devices: row.verify_devices ?? 1,
|
verify_devices: row.verify_devices ?? 1,
|
||||||
})),
|
})),
|
||||||
|
domain_settings: cloneRows(payload.domain_settings || []),
|
||||||
user_revisions: cloneRows(payload.user_revisions || []),
|
user_revisions: cloneRows(payload.user_revisions || []),
|
||||||
folders: cloneRows(payload.folders || []),
|
folders: cloneRows(payload.folders || []),
|
||||||
ciphers: cloneRows(payload.ciphers || []).map((row) => ({
|
ciphers: cloneRows(payload.ciphers || []).map((row) => ({
|
||||||
@@ -594,7 +599,7 @@ async function importBackupRows(db: D1Database, payload: BackupPayload['db'], us
|
|||||||
buildInsertStatements(
|
buildInsertStatements(
|
||||||
db,
|
db,
|
||||||
tableName('users'),
|
tableName('users'),
|
||||||
['id', 'email', 'name', 'master_password_hint', 'master_password_hash', 'key', 'private_key', 'public_key', 'kdf_type', 'kdf_iterations', 'kdf_memory', 'kdf_parallelism', 'security_stamp', 'role', 'status', 'verify_devices', 'totp_secret', 'totp_recovery_code', 'api_key', 'created_at', 'updated_at'],
|
['id', 'email', 'name', 'master_password_hint', 'master_password_hash', 'key', 'private_key', 'public_key', 'kdf_type', 'kdf_iterations', 'kdf_memory', 'kdf_parallelism', 'security_stamp', 'role', 'status', 'verify_devices', 'totp_secret', 'totp_recovery_code', 'created_at', 'updated_at'],
|
||||||
payload.users || []
|
payload.users || []
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -603,6 +608,17 @@ async function importBackupRows(db: D1Database, payload: BackupPayload['db'], us
|
|||||||
tableName('user_revisions'),
|
tableName('user_revisions'),
|
||||||
buildInsertStatements(db, tableName('user_revisions'), ['user_id', 'revision_date'], payload.user_revisions || [], true)
|
buildInsertStatements(db, tableName('user_revisions'), ['user_id', 'revision_date'], payload.user_revisions || [], true)
|
||||||
);
|
);
|
||||||
|
await runInsertBatch(
|
||||||
|
db,
|
||||||
|
tableName('domain_settings'),
|
||||||
|
buildInsertStatements(
|
||||||
|
db,
|
||||||
|
tableName('domain_settings'),
|
||||||
|
['user_id', 'equivalent_domains', 'custom_equivalent_domains', 'excluded_global_equivalent_domains', 'updated_at'],
|
||||||
|
payload.domain_settings || [],
|
||||||
|
true
|
||||||
|
)
|
||||||
|
);
|
||||||
await runInsertBatch(
|
await runInsertBatch(
|
||||||
db,
|
db,
|
||||||
tableName('folders'),
|
tableName('folders'),
|
||||||
@@ -729,6 +745,7 @@ export async function importBackupArchiveBytes(
|
|||||||
imported: {
|
imported: {
|
||||||
config: (db.config || []).length,
|
config: (db.config || []).length,
|
||||||
users: (db.users || []).length,
|
users: (db.users || []).length,
|
||||||
|
domainSettings: (db.domain_settings || []).length,
|
||||||
userRevisions: (db.user_revisions || []).length,
|
userRevisions: (db.user_revisions || []).length,
|
||||||
folders: (db.folders || []).length,
|
folders: (db.folders || []).length,
|
||||||
ciphers: (db.ciphers || []).length,
|
ciphers: (db.ciphers || []).length,
|
||||||
@@ -870,6 +887,7 @@ export async function importRemoteBackupArchiveBytes(
|
|||||||
imported: {
|
imported: {
|
||||||
config: (db.config || []).length,
|
config: (db.config || []).length,
|
||||||
users: (db.users || []).length,
|
users: (db.users || []).length,
|
||||||
|
domainSettings: (db.domain_settings || []).length,
|
||||||
userRevisions: (db.user_revisions || []).length,
|
userRevisions: (db.user_revisions || []).length,
|
||||||
folders: (db.folders || []).length,
|
folders: (db.folders || []).length,
|
||||||
ciphers: (db.ciphers || []).length,
|
ciphers: (db.ciphers || []).length,
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export interface RemoteBackupBrowserResponse {
|
|||||||
export interface AdminBackupImportCounts {
|
export interface AdminBackupImportCounts {
|
||||||
config: number;
|
config: number;
|
||||||
users: number;
|
users: number;
|
||||||
|
domainSettings?: number;
|
||||||
userRevisions: number;
|
userRevisions: number;
|
||||||
folders: number;
|
folders: number;
|
||||||
ciphers: number;
|
ciphers: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user