feat: add shared API utilities for handling requests and responses

- Introduced `shared.ts` with utility functions for API interactions, including JSON parsing, error handling, and content disposition parsing.
- Added `vault.ts` to manage vault-related operations such as folder and cipher management, including creation, deletion, and bulk operations.
- Implemented encryption and decryption methods for secure data handling within the vault.
- Created `backup-settings-repair.ts` to automatically repair backup settings for admin profiles if needed.
This commit is contained in:
shuaiplus
2026-03-15 04:17:09 +08:00
parent 1fcfeb91d1
commit f0ace28bf2
30 changed files with 2697 additions and 2519 deletions
+255
View File
@@ -0,0 +1,255 @@
import { t } from '../i18n';
import type {
BackupDestinationConfig,
BackupDestinationRecord,
BackupDestinationType,
BackupRuntimeState,
BackupScheduleConfig,
BackupSettings as AdminBackupSettings,
E3BackupDestination,
WebDavBackupDestination,
} from '@shared/backup-schema';
import {
parseContentDispositionFileName,
parseErrorMessage,
parseJson,
type AuthedFetch,
} from './shared';
export type {
BackupDestinationConfig,
BackupDestinationRecord,
BackupDestinationType,
BackupRuntimeState,
BackupScheduleConfig,
AdminBackupSettings,
E3BackupDestination,
WebDavBackupDestination,
};
export interface BackupSettingsPortableWrap {
userId: string;
wrappedKey: string;
}
export interface BackupSettingsPortablePayload {
iv: string;
ciphertext: string;
wraps: BackupSettingsPortableWrap[];
}
export interface BackupSettingsRepairStateResponse {
object: 'backup-settings-repair';
needsRepair: boolean;
portable: BackupSettingsPortablePayload | null;
}
export interface AdminBackupRunResponse {
object: 'backup-run';
result: {
fileName: string;
fileSize: number;
provider: string;
remotePath: string;
};
settings: AdminBackupSettings;
}
export interface RemoteBackupItem {
path: string;
name: string;
isDirectory: boolean;
size: number | null;
modifiedAt: string | null;
}
export interface RemoteBackupBrowserResponse {
object: 'backup-remote-browser';
destinationId: string;
destinationName: string;
provider: BackupDestinationType;
currentPath: string;
parentPath: string | null;
items: RemoteBackupItem[];
}
export interface AdminBackupImportCounts {
config: number;
users: number;
userRevisions: number;
folders: number;
ciphers: number;
attachments: number;
sends: number;
attachmentFiles: number;
sendFiles: number;
}
export interface AdminBackupImportResponse {
object: 'instance-backup-import';
imported: AdminBackupImportCounts;
}
export interface AdminBackupExportPayload {
fileName: string;
mimeType: string;
bytes: Uint8Array;
}
export async function exportAdminBackup(authedFetch: AuthedFetch): Promise<AdminBackupExportPayload> {
const resp = await authedFetch('/api/admin/backup/export', { method: 'POST' });
if (!resp.ok) throw new Error(await parseErrorMessage(resp, t('txt_backup_export_failed')));
const mimeType = String(resp.headers.get('Content-Type') || 'application/zip').trim() || 'application/zip';
const fileName = parseContentDispositionFileName(resp, 'nodewarden_backup.zip');
const bytes = new Uint8Array(await resp.arrayBuffer());
return { fileName, mimeType, bytes };
}
export async function getAdminBackupSettings(authedFetch: AuthedFetch): Promise<AdminBackupSettings> {
const resp = await authedFetch('/api/admin/backup/settings', { method: 'GET' });
if (!resp.ok) throw new Error(await parseErrorMessage(resp, t('txt_backup_settings_load_failed')));
const body = await parseJson<AdminBackupSettings>(resp);
if (!Array.isArray(body?.destinations)) throw new Error(t('txt_backup_settings_invalid_response'));
return body;
}
export async function saveAdminBackupSettings(
authedFetch: AuthedFetch,
settings: AdminBackupSettings
): Promise<AdminBackupSettings> {
const resp = await authedFetch('/api/admin/backup/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings),
});
if (!resp.ok) throw new Error(await parseErrorMessage(resp, t('txt_backup_settings_save_failed')));
const body = await parseJson<AdminBackupSettings>(resp);
if (!Array.isArray(body?.destinations)) throw new Error(t('txt_backup_settings_invalid_response'));
return body;
}
export async function getAdminBackupSettingsRepairState(
authedFetch: AuthedFetch
): Promise<BackupSettingsRepairStateResponse> {
const resp = await authedFetch('/api/admin/backup/settings/repair', { method: 'GET' });
if (!resp.ok) throw new Error(await parseErrorMessage(resp, t('txt_backup_settings_load_failed')));
const body = await parseJson<BackupSettingsRepairStateResponse>(resp);
if (!body || typeof body.needsRepair !== 'boolean') {
throw new Error(t('txt_backup_settings_invalid_response'));
}
return body;
}
export async function repairAdminBackupSettings(
authedFetch: AuthedFetch,
settings: AdminBackupSettings
): Promise<AdminBackupSettings> {
const resp = await authedFetch('/api/admin/backup/settings/repair', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings),
});
if (!resp.ok) throw new Error(await parseErrorMessage(resp, t('txt_backup_settings_save_failed')));
const body = await parseJson<AdminBackupSettings>(resp);
if (!Array.isArray(body?.destinations)) throw new Error(t('txt_backup_settings_invalid_response'));
return body;
}
export async function runAdminBackupNow(
authedFetch: AuthedFetch,
destinationId?: string | null
): Promise<AdminBackupRunResponse> {
const resp = await authedFetch('/api/admin/backup/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(destinationId ? { destinationId } : {}),
});
if (!resp.ok) throw new Error(await parseErrorMessage(resp, t('txt_backup_remote_run_failed')));
const body = await parseJson<AdminBackupRunResponse>(resp);
if (!body?.result || !body?.settings) throw new Error(t('txt_backup_remote_run_invalid_response'));
return body;
}
export async function listRemoteBackups(
authedFetch: AuthedFetch,
destinationId: string,
path: string = ''
): Promise<RemoteBackupBrowserResponse> {
const params = new URLSearchParams();
params.set('destinationId', destinationId);
if (path) params.set('path', path);
const query = `?${params.toString()}`;
const resp = await authedFetch(`/api/admin/backup/remote${query}`, { method: 'GET' });
if (!resp.ok) throw new Error(await parseErrorMessage(resp, t('txt_backup_remote_load_failed')));
const body = await parseJson<RemoteBackupBrowserResponse>(resp);
if (!body?.items || typeof body.currentPath !== 'string' || !body.destinationId) throw new Error(t('txt_backup_remote_invalid_response'));
return body;
}
export async function downloadRemoteBackup(
authedFetch: AuthedFetch,
destinationId: string,
path: string
): Promise<AdminBackupExportPayload> {
const params = new URLSearchParams();
params.set('destinationId', destinationId);
params.set('path', path);
const resp = await authedFetch(`/api/admin/backup/remote/download?${params.toString()}`, { method: 'GET' });
if (!resp.ok) throw new Error(await parseErrorMessage(resp, t('txt_backup_remote_download_failed')));
const mimeType = String(resp.headers.get('Content-Type') || 'application/zip').trim() || 'application/zip';
const fileName = parseContentDispositionFileName(resp, 'nodewarden_remote_backup.zip');
const bytes = new Uint8Array(await resp.arrayBuffer());
return { fileName, mimeType, bytes };
}
export async function deleteRemoteBackup(
authedFetch: AuthedFetch,
destinationId: string,
path: string
): Promise<void> {
const params = new URLSearchParams();
params.set('destinationId', destinationId);
params.set('path', path);
const resp = await authedFetch(`/api/admin/backup/remote/file?${params.toString()}`, { method: 'DELETE' });
if (!resp.ok) throw new Error(await parseErrorMessage(resp, t('txt_backup_remote_delete_failed')));
}
export async function restoreRemoteBackup(
authedFetch: AuthedFetch,
destinationId: string,
path: string,
replaceExisting: boolean = false
): Promise<AdminBackupImportResponse> {
const resp = await authedFetch('/api/admin/backup/remote/restore', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ destinationId, path, replaceExisting }),
});
if (!resp.ok) throw new Error(await parseErrorMessage(resp, t('txt_backup_remote_restore_failed')));
const body = await parseJson<AdminBackupImportResponse>(resp);
if (!body?.imported) throw new Error(t('txt_backup_remote_restore_invalid_response'));
return body;
}
export async function importAdminBackup(
authedFetch: AuthedFetch,
file: File,
replaceExisting: boolean = false
): Promise<AdminBackupImportResponse> {
const formData = new FormData();
formData.set('file', file, file.name || 'nodewarden_backup.zip');
if (replaceExisting) {
formData.set('replaceExisting', '1');
}
const resp = await authedFetch('/api/admin/backup/import', {
method: 'POST',
body: formData,
});
if (!resp.ok) throw new Error(await parseErrorMessage(resp, t('txt_backup_import_failed')));
const body = await parseJson<AdminBackupImportResponse>(resp);
if (!body?.imported) throw new Error(t('txt_backup_import_invalid_response'));
return body;
}