Refactor VaultPage component: remove exposed password checks, add bulk delete functionality for folders, and improve list rendering performance

- Removed password breach checking logic and related state management from VaultPage.
- Introduced bulk delete functionality for folders with a confirmation dialog.
- Enhanced list rendering with virtualization to improve performance.
- Updated styles for folder actions and list items for better UI consistency.
- Removed unused password breach library and related translations.
This commit is contained in:
shuaiplus
2026-03-11 02:22:35 +08:00
parent bc5efbf2fd
commit f4d2e7932a
11 changed files with 491 additions and 490 deletions
+31 -17
View File
@@ -34,6 +34,7 @@ import {
handleRestoreCipher,
handlePartialUpdateCipher,
handleBulkMoveCiphers,
handleBulkDeleteCiphers,
} from './handlers/ciphers';
// Folder handlers
@@ -42,7 +43,8 @@ import {
handleGetFolder,
handleCreateFolder,
handleUpdateFolder,
handleDeleteFolder
handleDeleteFolder,
handleBulkDeleteFolders,
} from './handlers/folders';
// Send handlers
@@ -55,6 +57,7 @@ import {
handleUploadSendFile,
handleUpdateSend,
handleDeleteSend,
handleBulkDeleteSends,
handleRemoveSendPassword,
handleRemoveSendAuth,
handleAccessSend,
@@ -141,6 +144,18 @@ function getNwIconSvg(): string {
return `<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96" role="img" aria-label="NW icon"><rect x="4" y="4" width="88" height="88" rx="20" fill="#111418"/><text x="48" y="60" text-anchor="middle" font-size="36" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-weight="800" letter-spacing="0.5" fill="#FFFFFF">NW</text></svg>`;
}
function isImportBypassRequest(request: Request, path: string, method: string): boolean {
if (request.headers.get('X-NodeWarden-Import') !== '1') return false;
if (method === 'POST') {
if (path === '/api/ciphers/import') return true;
if (/^\/api\/ciphers\/[a-f0-9-]+\/attachment\/v2$/i.test(path)) return true;
if (/^\/api\/ciphers\/[a-f0-9-]+\/attachment\/[a-f0-9-]+$/i.test(path)) return true;
}
return false;
}
function handleNwFavicon(): Response {
return new Response(getNwIconSvg(), {
status: 200,
@@ -151,17 +166,6 @@ function handleNwFavicon(): Response {
});
}
const BITWARDEN_DEFAULT_ICON_SHA256 = 'aaa64871332ad5b7d28fe8874efb19c2d9cc2f1e6de75d52b080b438225a0783';
function bytesToHex(bytes: Uint8Array): string {
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
}
async function sha256Hex(buffer: ArrayBuffer): Promise<string> {
const digest = await crypto.subtle.digest('SHA-256', buffer);
return bytesToHex(new Uint8Array(digest));
}
function isValidIconHostname(hostname: string): boolean {
if (!hostname) return false;
if (hostname.length > 253) return false;
@@ -183,7 +187,7 @@ function isValidIconHostname(hostname: string): boolean {
});
}
// Icons handler - proxy to Bitwarden's official icon service
// Icons handler - proxy to favicon.im
async function handleGetIcon(request: Request, env: Env, hostname: string): Promise<Response> {
try {
void env;
@@ -199,8 +203,7 @@ async function handleGetIcon(request: Request, env: Env, hostname: string): Prom
return cached;
}
// Use Bitwarden's official icon service
const iconUrl = `https://icons.bitwarden.net/${normalizedHostname}/icon.png`;
const iconUrl = `https://favicon.im/${normalizedHostname}`;
const resp = await fetch(iconUrl, {
headers: { 'User-Agent': 'NodeWarden/1.0' },
redirect: 'follow',
@@ -212,7 +215,7 @@ async function handleGetIcon(request: Request, env: Env, hostname: string): Prom
if (resp.ok) {
const body = await resp.arrayBuffer();
if (body.byteLength === 500 && (await sha256Hex(body)) === BITWARDEN_DEFAULT_ICON_SHA256) {
if (body.byteLength === 0) {
return new Response(null, { status: 204 });
}
const iconResponse = new Response(body, {
@@ -512,7 +515,7 @@ export async function handleRequest(request: Request, env: Env): Promise<Respons
return errorResponse('Account is disabled', 403);
}
// Unified rate limiting for all authenticated API requests.
{
if (!isImportBypassRequest(request, path, method)) {
const rateLimit = new RateLimitService(env.DB);
const rateLimitCheck = await rateLimit.consumeBudget(
userId + ':api',
@@ -600,6 +603,10 @@ export async function handleRequest(request: Request, env: Env): Promise<Respons
return handleCiphersImport(request, env, userId);
}
if (path === '/api/ciphers/delete' && method === 'POST') {
return handleBulkDeleteCiphers(request, env, userId);
}
// Bulk cipher operations (only move is allowed)
if (path === '/api/ciphers/move') {
if (method === 'POST' || method === 'PUT') {
@@ -677,6 +684,9 @@ export async function handleRequest(request: Request, env: Env): Promise<Respons
if (method === 'GET') return handleGetFolders(request, env, userId);
if (method === 'POST') return handleCreateFolder(request, env, userId);
}
if (path === '/api/folders/delete' && method === 'POST') {
return handleBulkDeleteFolders(request, env, userId);
}
// Match /api/folders/:id patterns
const folderMatch = path.match(/^\/api\/folders\/([a-f0-9-]+)$/i);
@@ -712,6 +722,10 @@ export async function handleRequest(request: Request, env: Env): Promise<Respons
if (method === 'POST') return handleCreateSend(request, env, userId);
}
if (path === '/api/sends/delete' && method === 'POST') {
return handleBulkDeleteSends(request, env, userId);
}
if ((path === '/api/sends/file/v2' || path === '/api/sends/file') && method === 'POST') {
return handleCreateFileSendV2(request, env, userId);
}