mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-11-04 10:40:20 +00:00
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:
1 parent
1fcfeb91d1
commit
f0ace28bf2
30 files changed
+2697
-2519
No files matched your search
@@ -0,0 +1,312 @@
|
||||
import { lazy, Suspense } from 'preact/compat';
|
||||
import { Link, Route, Switch } from 'wouter';
|
||||
import { ArrowUpDown, Cloud, LogOut, Settings as SettingsIcon, Shield, ShieldUser } from 'lucide-preact';
|
||||
import type { ImportAttachmentFile, ImportResultSummary } from '@/components/ImportPage';
|
||||
import SendsPage from '@/components/SendsPage';
|
||||
import TotpCodesPage from '@/components/TotpCodesPage';
|
||||
import VaultPage from '@/components/VaultPage';
|
||||
import type { AdminBackupRunResponse, AdminBackupSettings, RemoteBackupBrowserResponse } from '@/lib/api/backup';
|
||||
import type { CiphersImportPayload } from '@/lib/api/vault';
|
||||
import { t } from '@/lib/i18n';
|
||||
import type { AdminInvite, AdminUser, AuthorizedDevice, Cipher, Folder as VaultFolder, Profile, Send, SendDraft, SessionState, VaultDraft } from '@/lib/types';
|
||||
import type { ExportRequest } from '@/lib/export-formats';
|
||||
|
||||
const SettingsPage = lazy(() => import('@/components/SettingsPage'));
|
||||
const SecurityDevicesPage = lazy(() => import('@/components/SecurityDevicesPage'));
|
||||
const AdminPage = lazy(() => import('@/components/AdminPage'));
|
||||
const BackupCenterPage = lazy(() => import('@/components/BackupCenterPage'));
|
||||
const ImportPage = lazy(() => import('@/components/ImportPage'));
|
||||
|
||||
function RouteContentFallback() {
|
||||
return <div className="loading-screen">{t('txt_loading_nodewarden')}</div>;
|
||||
}
|
||||
|
||||
interface AppMainRoutesProps {
|
||||
profile: Profile | null;
|
||||
session: SessionState | null;
|
||||
mobileLayout: boolean;
|
||||
importRoute: string;
|
||||
settingsHomeRoute: string;
|
||||
settingsAccountRoute: string;
|
||||
decryptedCiphers: Cipher[];
|
||||
decryptedFolders: VaultFolder[];
|
||||
decryptedSends: Send[];
|
||||
ciphersLoading: boolean;
|
||||
foldersLoading: boolean;
|
||||
sendsLoading: boolean;
|
||||
users: AdminUser[];
|
||||
invites: AdminInvite[];
|
||||
totpEnabled: boolean;
|
||||
authorizedDevices: AuthorizedDevice[];
|
||||
authorizedDevicesLoading: boolean;
|
||||
onNavigate: (path: string) => void;
|
||||
onLogout: () => void;
|
||||
onNotify: (type: 'success' | 'error' | 'warning', text: string) => void;
|
||||
onImport: (
|
||||
payload: CiphersImportPayload,
|
||||
options: { folderMode: 'original' | 'none' | 'target'; targetFolderId: string | null },
|
||||
attachments?: ImportAttachmentFile[]
|
||||
) => Promise<ImportResultSummary>;
|
||||
onImportEncryptedRaw: (
|
||||
payload: CiphersImportPayload,
|
||||
options: { folderMode: 'original' | 'none' | 'target'; targetFolderId: string | null },
|
||||
attachments?: ImportAttachmentFile[]
|
||||
) => Promise<ImportResultSummary>;
|
||||
onExport: (request: ExportRequest) => Promise<void>;
|
||||
onCreateVaultItem: (draft: VaultDraft, attachments?: File[]) => Promise<void>;
|
||||
onUpdateVaultItem: (cipher: Cipher, draft: VaultDraft, options?: { addFiles?: File[]; removeAttachmentIds?: string[] }) => Promise<void>;
|
||||
onDeleteVaultItem: (cipher: Cipher) => Promise<void>;
|
||||
onBulkDeleteVaultItems: (ids: string[]) => Promise<void>;
|
||||
onBulkPermanentDeleteVaultItems: (ids: string[]) => Promise<void>;
|
||||
onBulkRestoreVaultItems: (ids: string[]) => Promise<void>;
|
||||
onBulkMoveVaultItems: (ids: string[], folderId: string | null) => Promise<void>;
|
||||
onVerifyMasterPassword: (email: string, password: string) => Promise<void>;
|
||||
onCreateFolder: (name: string) => Promise<void>;
|
||||
onDeleteFolder: (folderId: string) => Promise<void>;
|
||||
onBulkDeleteFolders: (folderIds: string[]) => Promise<void>;
|
||||
onDownloadVaultAttachment: (cipher: Cipher, attachmentId: string) => Promise<void>;
|
||||
onRefreshVault: () => Promise<void>;
|
||||
onCreateSend: (draft: SendDraft, autoCopyLink: boolean) => Promise<void>;
|
||||
onUpdateSend: (send: Send, draft: SendDraft, autoCopyLink: boolean) => Promise<void>;
|
||||
onDeleteSend: (send: Send) => Promise<void>;
|
||||
onBulkDeleteSends: (ids: string[]) => Promise<void>;
|
||||
onChangePassword: (currentPassword: string, nextPassword: string, nextPassword2: string) => Promise<void>;
|
||||
onEnableTotp: (secret: string, token: string) => Promise<void>;
|
||||
onOpenDisableTotp: () => void;
|
||||
onGetRecoveryCode: (masterPassword: string) => Promise<string>;
|
||||
onRefreshAuthorizedDevices: () => Promise<void>;
|
||||
onRevokeDeviceTrust: (device: AuthorizedDevice) => void;
|
||||
onRemoveDevice: (device: AuthorizedDevice) => void;
|
||||
onRevokeAllDeviceTrust: () => void;
|
||||
onRemoveAllDevices: () => void;
|
||||
onCreateInvite: (hours: number) => Promise<void>;
|
||||
onRefreshAdmin: () => void;
|
||||
onDeleteAllInvites: () => Promise<void>;
|
||||
onToggleUserStatus: (userId: string, status: 'active' | 'banned') => Promise<void>;
|
||||
onDeleteUser: (userId: string) => Promise<void>;
|
||||
onRevokeInvite: (code: string) => Promise<void>;
|
||||
onExportBackup: () => Promise<void>;
|
||||
onImportBackup: (file: File, replaceExisting?: boolean) => Promise<void>;
|
||||
onLoadBackupSettings: () => Promise<AdminBackupSettings>;
|
||||
onSaveBackupSettings: (settings: AdminBackupSettings) => Promise<AdminBackupSettings>;
|
||||
onRunRemoteBackup: (destinationId?: string | null) => Promise<AdminBackupRunResponse>;
|
||||
onListRemoteBackups: (destinationId: string, path: string) => Promise<RemoteBackupBrowserResponse>;
|
||||
onDownloadRemoteBackup: (destinationId: string, path: string) => Promise<void>;
|
||||
onDeleteRemoteBackup: (destinationId: string, path: string) => Promise<void>;
|
||||
onRestoreRemoteBackup: (destinationId: string, path: string, replaceExisting?: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
export default function AppMainRoutes(props: AppMainRoutesProps) {
|
||||
const importRoutePaths = [props.importRoute, '/tools/import', '/tools/import-export', '/tools/import-data', '/import', '/import-export'] as const;
|
||||
const importPageContent = (
|
||||
<Suspense fallback={<RouteContentFallback />}>
|
||||
<ImportPage
|
||||
onImport={props.onImport}
|
||||
onImportEncryptedRaw={props.onImportEncryptedRaw}
|
||||
accountKeys={props.session?.symEncKey && props.session?.symMacKey ? { encB64: props.session.symEncKey, macB64: props.session.symMacKey } : null}
|
||||
onNotify={props.onNotify}
|
||||
folders={props.decryptedFolders}
|
||||
onExport={props.onExport}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
const renderImportPageRoute = () => (
|
||||
<div className="stack">
|
||||
{props.mobileLayout && (
|
||||
<div className="mobile-settings-subhead">
|
||||
<button type="button" className="btn btn-secondary small mobile-settings-back" onClick={() => props.onNavigate(props.settingsHomeRoute)}>
|
||||
<span className="btn-icon" aria-hidden="true">{"<"}</span>
|
||||
{t('txt_back')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{importPageContent}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route path="/sends">
|
||||
<SendsPage
|
||||
sends={props.decryptedSends}
|
||||
loading={props.sendsLoading}
|
||||
onRefresh={props.onRefreshVault}
|
||||
onCreate={props.onCreateSend}
|
||||
onUpdate={props.onUpdateSend}
|
||||
onDelete={props.onDeleteSend}
|
||||
onBulkDelete={props.onBulkDeleteSends}
|
||||
onNotify={props.onNotify}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/vault/totp">
|
||||
<TotpCodesPage ciphers={props.decryptedCiphers} loading={props.ciphersLoading} onNotify={props.onNotify} />
|
||||
</Route>
|
||||
<Route path="/vault">
|
||||
<VaultPage
|
||||
ciphers={props.decryptedCiphers}
|
||||
folders={props.decryptedFolders}
|
||||
loading={props.ciphersLoading || props.foldersLoading}
|
||||
emailForReprompt={props.profile?.email || props.session?.email || ''}
|
||||
onRefresh={props.onRefreshVault}
|
||||
onCreate={props.onCreateVaultItem}
|
||||
onUpdate={props.onUpdateVaultItem}
|
||||
onDelete={props.onDeleteVaultItem}
|
||||
onBulkDelete={props.onBulkDeleteVaultItems}
|
||||
onBulkPermanentDelete={props.onBulkPermanentDeleteVaultItems}
|
||||
onBulkRestore={props.onBulkRestoreVaultItems}
|
||||
onBulkMove={props.onBulkMoveVaultItems}
|
||||
onVerifyMasterPassword={props.onVerifyMasterPassword}
|
||||
onNotify={props.onNotify}
|
||||
onCreateFolder={props.onCreateFolder}
|
||||
onDeleteFolder={props.onDeleteFolder}
|
||||
onBulkDeleteFolders={props.onBulkDeleteFolders}
|
||||
onDownloadAttachment={props.onDownloadVaultAttachment}
|
||||
/>
|
||||
</Route>
|
||||
<Route path={props.settingsAccountRoute}>
|
||||
{props.profile && (
|
||||
<div className="stack">
|
||||
{props.mobileLayout && (
|
||||
<div className="mobile-settings-subhead">
|
||||
<button type="button" className="btn btn-secondary small mobile-settings-back" onClick={() => props.onNavigate(props.settingsHomeRoute)}>
|
||||
<span className="btn-icon" aria-hidden="true">{"<"}</span>
|
||||
{t('txt_back')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<Suspense fallback={<RouteContentFallback />}>
|
||||
<SettingsPage
|
||||
profile={props.profile}
|
||||
totpEnabled={props.totpEnabled}
|
||||
onChangePassword={props.onChangePassword}
|
||||
onEnableTotp={props.onEnableTotp}
|
||||
onOpenDisableTotp={props.onOpenDisableTotp}
|
||||
onGetRecoveryCode={props.onGetRecoveryCode}
|
||||
onNotify={props.onNotify}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
)}
|
||||
</Route>
|
||||
<Route path="/settings">
|
||||
{props.profile && (
|
||||
<section className="card mobile-settings-card">
|
||||
<div className="mobile-settings-links">
|
||||
<Link href={props.settingsAccountRoute} className="mobile-settings-link">
|
||||
<SettingsIcon size={18} />
|
||||
<span>{t('nav_account_settings')}</span>
|
||||
</Link>
|
||||
<Link href="/security/devices" className="mobile-settings-link">
|
||||
<Shield size={18} />
|
||||
<span>{t('nav_device_management')}</span>
|
||||
</Link>
|
||||
<Link href={props.importRoute} className="mobile-settings-link">
|
||||
<ArrowUpDown size={18} />
|
||||
<span>{t('nav_import_export')}</span>
|
||||
</Link>
|
||||
{props.profile.role === 'admin' && (
|
||||
<Link href="/admin" className="mobile-settings-link">
|
||||
<ShieldUser size={18} />
|
||||
<span>{t('nav_admin_panel')}</span>
|
||||
</Link>
|
||||
)}
|
||||
{props.profile.role === 'admin' && (
|
||||
<Link href="/help" className="mobile-settings-link">
|
||||
<Cloud size={18} />
|
||||
<span>{t('nav_backup_strategy')}</span>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<button type="button" className="btn btn-secondary mobile-settings-logout" onClick={props.onLogout}>
|
||||
<LogOut size={14} className="btn-icon" />
|
||||
{t('txt_sign_out')}
|
||||
</button>
|
||||
</section>
|
||||
)}
|
||||
</Route>
|
||||
<Route path="/security/devices">
|
||||
<div className="stack">
|
||||
{props.mobileLayout && (
|
||||
<div className="mobile-settings-subhead">
|
||||
<button type="button" className="btn btn-secondary small mobile-settings-back" onClick={() => props.onNavigate(props.settingsHomeRoute)}>
|
||||
<span className="btn-icon" aria-hidden="true">{"<"}</span>
|
||||
{t('txt_back')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<Suspense fallback={<RouteContentFallback />}>
|
||||
<SecurityDevicesPage
|
||||
devices={props.authorizedDevices}
|
||||
loading={props.authorizedDevicesLoading}
|
||||
onRefresh={() => void props.onRefreshAuthorizedDevices()}
|
||||
onRevokeTrust={props.onRevokeDeviceTrust}
|
||||
onRemoveDevice={props.onRemoveDevice}
|
||||
onRevokeAll={props.onRevokeAllDeviceTrust}
|
||||
onRemoveAll={props.onRemoveAllDevices}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</Route>
|
||||
<Route path="/admin">
|
||||
<div className="stack">
|
||||
{props.mobileLayout && (
|
||||
<div className="mobile-settings-subhead">
|
||||
<button type="button" className="btn btn-secondary small mobile-settings-back" onClick={() => props.onNavigate(props.settingsHomeRoute)}>
|
||||
<span className="btn-icon" aria-hidden="true">{"<"}</span>
|
||||
{t('txt_back')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<Suspense fallback={<RouteContentFallback />}>
|
||||
<AdminPage
|
||||
currentUserId={props.profile?.id || ''}
|
||||
users={props.users}
|
||||
invites={props.invites}
|
||||
onRefresh={props.onRefreshAdmin}
|
||||
onCreateInvite={props.onCreateInvite}
|
||||
onDeleteAllInvites={props.onDeleteAllInvites}
|
||||
onToggleUserStatus={props.onToggleUserStatus}
|
||||
onDeleteUser={props.onDeleteUser}
|
||||
onRevokeInvite={props.onRevokeInvite}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</Route>
|
||||
{importRoutePaths.map((path) => (
|
||||
<Route key={path} path={path}>
|
||||
{renderImportPageRoute()}
|
||||
</Route>
|
||||
))}
|
||||
<Route path="/help">
|
||||
{props.profile?.role === 'admin' ? (
|
||||
<div className="stack">
|
||||
{props.mobileLayout && (
|
||||
<div className="mobile-settings-subhead">
|
||||
<button type="button" className="btn btn-secondary small mobile-settings-back" onClick={() => props.onNavigate(props.settingsHomeRoute)}>
|
||||
<span className="btn-icon" aria-hidden="true">{"<"}</span>
|
||||
{t('txt_back')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<Suspense fallback={<RouteContentFallback />}>
|
||||
<BackupCenterPage
|
||||
currentUserId={props.profile?.id || null}
|
||||
onExport={props.onExportBackup}
|
||||
onImport={props.onImportBackup}
|
||||
onLoadSettings={props.onLoadBackupSettings}
|
||||
onListRemoteBackups={props.onListRemoteBackups}
|
||||
onDownloadRemoteBackup={props.onDownloadRemoteBackup}
|
||||
onDeleteRemoteBackup={props.onDeleteRemoteBackup}
|
||||
onRestoreRemoteBackup={props.onRestoreRemoteBackup}
|
||||
onSaveSettings={props.onSaveBackupSettings}
|
||||
onRunRemoteBackup={props.onRunRemoteBackup}
|
||||
onNotify={props.onNotify}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
) : null}
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type BackupDestinationRecord,
|
||||
type BackupDestinationType,
|
||||
type RemoteBackupBrowserResponse,
|
||||
} from '@/lib/api';
|
||||
} from '@/lib/api/backup';
|
||||
import {
|
||||
REMOTE_BROWSER_ITEMS_PER_PAGE,
|
||||
compareRemoteItems,
|
||||
@@ -92,8 +92,8 @@ export default function BackupCenterPage(props: BackupCenterPageProps) {
|
||||
const selectedRecommendedProvider = RECOMMENDED_PROVIDERS.find((provider) => provider.id === selectedProviderId) || null;
|
||||
const recommendedWebDavProviders = RECOMMENDED_PROVIDERS.filter((provider) => provider.protocol === 'webdav');
|
||||
const recommendedS3Providers = RECOMMENDED_PROVIDERS.filter((provider) => provider.protocol === 's3');
|
||||
const canRunSelectedDestination = !!selectedDestination && selectedDestination.type !== 'placeholder' && selectedDestinationIsSaved;
|
||||
const canBrowseSelectedDestination = !!savedSelectedDestination && savedSelectedDestination.type !== 'placeholder';
|
||||
const canRunSelectedDestination = !!selectedDestination && selectedDestinationIsSaved;
|
||||
const canBrowseSelectedDestination = !!savedSelectedDestination;
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -135,12 +135,6 @@ export default function BackupCenterPage(props: BackupCenterPageProps) {
|
||||
});
|
||||
}, [props.currentUserId, remoteBrowserCache, remoteBrowserPageByKey, remoteBrowserPathByDestination, selectedDestinationId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedDestination?.type === 'placeholder') {
|
||||
setSelectedDestinationId(getFirstVisibleDestinationId(settings));
|
||||
}
|
||||
}, [selectedDestination?.id, selectedDestination?.type, settings]);
|
||||
|
||||
function updateSettings(mutator: (current: AdminBackupSettings) => AdminBackupSettings) {
|
||||
setSettings((current) => {
|
||||
const next = mutator(current);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { strFromU8, unzipSync } from 'fflate';
|
||||
import { BlobReader, Uint8ArrayWriter, ZipReader, configure as configureZipJs } from '@zip.js/zip.js';
|
||||
import { Download, FileUp } from 'lucide-preact';
|
||||
import ConfirmDialog from '@/components/ConfirmDialog';
|
||||
import type { CiphersImportPayload } from '@/lib/api';
|
||||
import type { CiphersImportPayload } from '@/lib/api/vault';
|
||||
import {
|
||||
type EncryptedJsonMode,
|
||||
EXPORT_FORMATS,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
import { Download, Eye, Lock } from 'lucide-preact';
|
||||
import { accessPublicSend, accessPublicSendFile, decryptPublicSend, decryptPublicSendFileBytes } from '@/lib/api';
|
||||
import { accessPublicSend, accessPublicSendFile, decryptPublicSend, decryptPublicSendFileBytes } from '@/lib/api/send';
|
||||
import StandalonePageFrame from '@/components/StandalonePageFrame';
|
||||
import { t } from '@/lib/i18n';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
E3BackupDestination,
|
||||
RemoteBackupBrowserResponse,
|
||||
WebDavBackupDestination,
|
||||
} from '@/lib/api';
|
||||
} from '@/lib/api/backup';
|
||||
import { COMMON_TIME_ZONES, WEEKDAY_OPTIONS, getDestinationTypeLabel } from '@/lib/backup-center';
|
||||
import type { RecommendedProvider } from '@/lib/backup-recommendations';
|
||||
import { RemoteBackupBrowser } from './RemoteBackupBrowser';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Plus } from 'lucide-preact';
|
||||
import type { BackupDestinationRecord, BackupDestinationType } from '@/lib/api';
|
||||
import type { BackupDestinationRecord, BackupDestinationType } from '@/lib/api/backup';
|
||||
import { formatDateTime, getDestinationTypeLabel } from '@/lib/backup-center';
|
||||
import { t } from '@/lib/i18n';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Download, FileArchive, FolderOpen, RefreshCw, RotateCcw, Trash2 } from 'lucide-preact';
|
||||
import type { RemoteBackupBrowserResponse } from '@/lib/api';
|
||||
import type { RemoteBackupBrowserResponse } from '@/lib/api/backup';
|
||||
import { formatBytes, formatDateTime, isZipCandidate } from '@/lib/backup-center';
|
||||
import { t } from '@/lib/i18n';
|
||||
|
||||
|
||||
Reference in new issue
Block a user