feat: add backup recommendations and update backup strategy UI

- Introduced new backup recommendations feature with interfaces for recommended storage providers.
- Updated i18n translations for backup strategy to reflect new terminology and improved descriptions.
- Enhanced types with optional private and public keys in user profiles.
- Redesigned backup-related styles for better layout and responsiveness.
- Updated TypeScript configuration to include shared modules.
- Configured Vite to resolve shared modules and allow filesystem access.
- Added cron triggers for periodic tasks in Wrangler configuration.
This commit is contained in:
shuaiplus
2026-03-15 03:34:16 +08:00
parent 51d0e60cf1
commit b1c6ec50da
29 changed files with 5662 additions and 951 deletions
+65
View File
@@ -0,0 +1,65 @@
import { base64ToBytes, decryptBw } from './crypto';
import type { AdminBackupSettings, BackupSettingsPortablePayload } from './api';
import type { Profile, SessionState } from './types';
const PORTABLE_ALGORITHM = 'RSA-OAEP';
const PORTABLE_HASH = 'SHA-1';
const AES_GCM_ALGORITHM = 'AES-GCM';
async function importPortablePrivateKey(pkcs8: Uint8Array): Promise<CryptoKey> {
return crypto.subtle.importKey(
'pkcs8',
pkcs8,
{ name: PORTABLE_ALGORITHM, hash: PORTABLE_HASH },
false,
['decrypt']
);
}
async function importPortableAesKey(keyBytes: Uint8Array): Promise<CryptoKey> {
return crypto.subtle.importKey('raw', keyBytes, { name: AES_GCM_ALGORITHM }, false, ['decrypt']);
}
export async function decryptPortableBackupSettings(
portable: BackupSettingsPortablePayload,
profile: Profile,
session: SessionState
): Promise<AdminBackupSettings> {
if (!profile.id) {
throw new Error('Current administrator profile is missing an id');
}
if (!profile.privateKey) {
throw new Error('Current administrator profile is missing a private key');
}
if (!session.symEncKey || !session.symMacKey) {
throw new Error('Current session is missing unlocked vault keys');
}
const wrap = portable.wraps.find((entry) => entry.userId === profile.id);
if (!wrap) {
throw new Error('No portable backup settings wrap is available for the current administrator');
}
const privateKeyBytes = await decryptBw(
profile.privateKey,
base64ToBytes(session.symEncKey),
base64ToBytes(session.symMacKey)
);
const privateKey = await importPortablePrivateKey(privateKeyBytes);
const portableDek = new Uint8Array(
await crypto.subtle.decrypt(
{ name: PORTABLE_ALGORITHM },
privateKey,
base64ToBytes(wrap.wrappedKey)
)
);
const aesKey = await importPortableAesKey(portableDek);
const plaintext = new Uint8Array(
await crypto.subtle.decrypt(
{ name: AES_GCM_ALGORITHM, iv: base64ToBytes(portable.iv) },
aesKey,
base64ToBytes(portable.ciphertext)
)
);
return JSON.parse(new TextDecoder().decode(plaintext)) as AdminBackupSettings;
}