mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-22 21:50:13 +00:00
Align web vault updates with resource sync
This commit is contained in:
@@ -102,6 +102,7 @@ async function processSendFileUpload(
|
|||||||
const storage = new StorageService(env.DB);
|
const storage = new StorageService(env.DB);
|
||||||
const revisionDate = await storage.updateRevisionDate(send.userId);
|
const revisionDate = await storage.updateRevisionDate(send.userId);
|
||||||
notifyVaultSyncForRequest(request, env, send.userId, revisionDate);
|
notifyVaultSyncForRequest(request, env, send.userId, revisionDate);
|
||||||
|
notifySendUpdateForRequest(request, env, send.id, send.userId, revisionDate);
|
||||||
|
|
||||||
return new Response(null, { status: 201 });
|
return new Response(null, { status: 201 });
|
||||||
}
|
}
|
||||||
|
|||||||
+87
-31
@@ -20,6 +20,7 @@ import {
|
|||||||
saveProfileSnapshot,
|
saveProfileSnapshot,
|
||||||
revokeCurrentSession,
|
revokeCurrentSession,
|
||||||
getTotpStatus,
|
getTotpStatus,
|
||||||
|
getVaultRevisionDate,
|
||||||
saveSession,
|
saveSession,
|
||||||
stripProfileSecrets,
|
stripProfileSecrets,
|
||||||
} from '@/lib/api/auth';
|
} from '@/lib/api/auth';
|
||||||
@@ -33,7 +34,7 @@ import { clearAuditLogs, getAuditLogSettings, listAdminInvites, listAdminUsers,
|
|||||||
import { getDomainRules, saveDomainRules } from '@/lib/api/domains';
|
import { getDomainRules, saveDomainRules } from '@/lib/api/domains';
|
||||||
import { getSendById, getSends } from '@/lib/api/send';
|
import { getSendById, getSends } from '@/lib/api/send';
|
||||||
import { getCipherById, getFolderById, repairCipherKeyMismatches, repairCipherUriChecksums } from '@/lib/api/vault';
|
import { getCipherById, getFolderById, repairCipherKeyMismatches, repairCipherUriChecksums } from '@/lib/api/vault';
|
||||||
import { getCachedVaultCoreSnapshot, invalidateVaultCoreSyncSnapshot, loadVaultCoreSyncSnapshot } from '@/lib/api/vault-sync';
|
import { getCachedVaultCoreSnapshot, invalidateVaultCoreSyncSnapshot, loadVaultCoreSyncSnapshot, saveVaultCoreSyncSnapshot } from '@/lib/api/vault-sync';
|
||||||
import { silentlyRepairBackupSettingsIfNeeded } from '@/lib/backup-settings-repair';
|
import { silentlyRepairBackupSettingsIfNeeded } from '@/lib/backup-settings-repair';
|
||||||
import {
|
import {
|
||||||
parseSignalRTextFrames,
|
parseSignalRTextFrames,
|
||||||
@@ -1361,7 +1362,15 @@ export default function App() {
|
|||||||
return items.filter((item) => String(item.id || '').trim() !== normalizedId);
|
return items.filter((item) => String(item.id || '').trim() !== normalizedId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchVaultCoreSnapshot(updater: (snapshot: VaultCoreSnapshot) => VaultCoreSnapshot): void {
|
function revisionStampFromIso(value: unknown): number | null {
|
||||||
|
const stamp = new Date(String(value || '').trim()).getTime();
|
||||||
|
return Number.isFinite(stamp) && stamp > 0 ? stamp : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchVaultCoreSnapshot(
|
||||||
|
updater: (snapshot: VaultCoreSnapshot) => VaultCoreSnapshot,
|
||||||
|
options?: { revisionStamp?: number | null }
|
||||||
|
): void {
|
||||||
if (!vaultCacheKey) return;
|
if (!vaultCacheKey) return;
|
||||||
let nextSnapshot: VaultCoreSnapshot | null = null;
|
let nextSnapshot: VaultCoreSnapshot | null = null;
|
||||||
queryClient.setQueryData(['vault-core', vaultCacheKey], (previous?: VaultCoreSnapshot) => {
|
queryClient.setQueryData(['vault-core', vaultCacheKey], (previous?: VaultCoreSnapshot) => {
|
||||||
@@ -1369,34 +1378,50 @@ export default function App() {
|
|||||||
nextSnapshot = updater(base);
|
nextSnapshot = updater(base);
|
||||||
return nextSnapshot;
|
return nextSnapshot;
|
||||||
});
|
});
|
||||||
if (nextSnapshot) setCachedVaultCore(nextSnapshot);
|
if (nextSnapshot) {
|
||||||
|
setCachedVaultCore(nextSnapshot);
|
||||||
|
void saveVaultCoreSyncSnapshot(vaultCacheKey, nextSnapshot, options?.revisionStamp ?? null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function upsertEncryptedCipher(cipher: Cipher): void {
|
async function refreshVaultCoreRevisionStamp(): Promise<void> {
|
||||||
|
if (!vaultCacheKey || !session?.accessToken) return;
|
||||||
|
try {
|
||||||
|
const revisionStamp = await getVaultRevisionDate(authedFetch);
|
||||||
|
const currentSnapshot = normalizeVaultCoreSnapshot(
|
||||||
|
queryClient.getQueryData<VaultCoreSnapshot>(['vault-core', vaultCacheKey]) || cachedVaultCore
|
||||||
|
);
|
||||||
|
await saveVaultCoreSyncSnapshot(vaultCacheKey, currentSnapshot, revisionStamp);
|
||||||
|
} catch {
|
||||||
|
// A stale revision stamp only affects the next cache validation; the local resource patch remains valid.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsertEncryptedCipher(cipher: Cipher, revisionStamp?: number | null): void {
|
||||||
patchVaultCoreSnapshot((snapshot) => ({
|
patchVaultCoreSnapshot((snapshot) => ({
|
||||||
...snapshot,
|
...snapshot,
|
||||||
ciphers: upsertById(snapshot.ciphers, cipher),
|
ciphers: upsertById(snapshot.ciphers, cipher),
|
||||||
}));
|
}), { revisionStamp: revisionStamp ?? revisionStampFromIso(cipher.revisionDate) });
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteCipherLocally(cipherId: string): void {
|
function deleteCipherLocally(cipherId: string, revisionStamp?: number | null): void {
|
||||||
const id = String(cipherId || '').trim();
|
const id = String(cipherId || '').trim();
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
patchVaultCoreSnapshot((snapshot) => ({
|
patchVaultCoreSnapshot((snapshot) => ({
|
||||||
...snapshot,
|
...snapshot,
|
||||||
ciphers: removeById(snapshot.ciphers, id),
|
ciphers: removeById(snapshot.ciphers, id),
|
||||||
}));
|
}), { revisionStamp });
|
||||||
setDecryptedCiphers((current) => removeById(current, id));
|
setDecryptedCiphers((current) => removeById(current, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
function upsertEncryptedFolder(folder: VaultFolder): void {
|
function upsertEncryptedFolder(folder: VaultFolder, revisionStamp?: number | null): void {
|
||||||
patchVaultCoreSnapshot((snapshot) => ({
|
patchVaultCoreSnapshot((snapshot) => ({
|
||||||
...snapshot,
|
...snapshot,
|
||||||
folders: upsertById(snapshot.folders, folder),
|
folders: upsertById(snapshot.folders, folder),
|
||||||
}));
|
}), { revisionStamp: revisionStamp ?? revisionStampFromIso(folder.revisionDate) });
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteFolderLocally(folderId: string): void {
|
function deleteFolderLocally(folderId: string, revisionStamp?: number | null): void {
|
||||||
const id = String(folderId || '').trim();
|
const id = String(folderId || '').trim();
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
patchVaultCoreSnapshot((snapshot) => ({
|
patchVaultCoreSnapshot((snapshot) => ({
|
||||||
@@ -1405,38 +1430,38 @@ export default function App() {
|
|||||||
ciphers: snapshot.ciphers.map((cipher) => (
|
ciphers: snapshot.ciphers.map((cipher) => (
|
||||||
String(cipher.folderId || '').trim() === id ? { ...cipher, folderId: null } : cipher
|
String(cipher.folderId || '').trim() === id ? { ...cipher, folderId: null } : cipher
|
||||||
)),
|
)),
|
||||||
}));
|
}), { revisionStamp });
|
||||||
setDecryptedFolders((current) => removeById(current, id));
|
setDecryptedFolders((current) => removeById(current, id));
|
||||||
setDecryptedCiphers((current) => current.map((cipher) => (
|
setDecryptedCiphers((current) => current.map((cipher) => (
|
||||||
String(cipher.folderId || '').trim() === id ? { ...cipher, folderId: null } : cipher
|
String(cipher.folderId || '').trim() === id ? { ...cipher, folderId: null } : cipher
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function upsertEncryptedSend(send: Send): void {
|
function upsertEncryptedSend(send: Send, revisionStamp?: number | null): void {
|
||||||
patchVaultCoreSnapshot((snapshot) => ({
|
patchVaultCoreSnapshot((snapshot) => ({
|
||||||
...snapshot,
|
...snapshot,
|
||||||
sends: upsertById(snapshot.sends, send),
|
sends: upsertById(snapshot.sends, send),
|
||||||
}));
|
}), { revisionStamp: revisionStamp ?? revisionStampFromIso(send.revisionDate) });
|
||||||
queryClient.setQueryData(sendsQueryKey, (previous?: Send[]) => upsertById(Array.isArray(previous) ? previous : [], send));
|
queryClient.setQueryData(sendsQueryKey, (previous?: Send[]) => upsertById(Array.isArray(previous) ? previous : [], send));
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSendLocally(sendId: string): void {
|
function deleteSendLocally(sendId: string, revisionStamp?: number | null): void {
|
||||||
const id = String(sendId || '').trim();
|
const id = String(sendId || '').trim();
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
patchVaultCoreSnapshot((snapshot) => ({
|
patchVaultCoreSnapshot((snapshot) => ({
|
||||||
...snapshot,
|
...snapshot,
|
||||||
sends: removeById(snapshot.sends, id),
|
sends: removeById(snapshot.sends, id),
|
||||||
}));
|
}), { revisionStamp });
|
||||||
queryClient.setQueryData(sendsQueryKey, (previous?: Send[]) => removeById(Array.isArray(previous) ? previous : [], id));
|
queryClient.setQueryData(sendsQueryKey, (previous?: Send[]) => removeById(Array.isArray(previous) ? previous : [], id));
|
||||||
setDecryptedSends((current) => removeById(current, id));
|
setDecryptedSends((current) => removeById(current, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function upsertCipherFromNotification(cipherId: string): Promise<void> {
|
async function upsertCipherFromNotification(cipherId: string, revisionStamp?: number | null): Promise<void> {
|
||||||
const id = String(cipherId || '').trim();
|
const id = String(cipherId || '').trim();
|
||||||
if (!id || !session?.symEncKey || !session?.symMacKey) return;
|
if (!id || !session?.symEncKey || !session?.symMacKey) return;
|
||||||
try {
|
try {
|
||||||
const encrypted = await getCipherById(authedFetch, id);
|
const encrypted = await getCipherById(authedFetch, id);
|
||||||
upsertEncryptedCipher(encrypted);
|
upsertEncryptedCipher(encrypted, revisionStamp);
|
||||||
const result = await decryptVaultCore({
|
const result = await decryptVaultCore({
|
||||||
folders: [],
|
folders: [],
|
||||||
ciphers: [encrypted],
|
ciphers: [encrypted],
|
||||||
@@ -1454,12 +1479,12 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function upsertFolderFromNotification(folderId: string): Promise<void> {
|
async function upsertFolderFromNotification(folderId: string, revisionStamp?: number | null): Promise<void> {
|
||||||
const id = String(folderId || '').trim();
|
const id = String(folderId || '').trim();
|
||||||
if (!id || !session?.symEncKey || !session?.symMacKey) return;
|
if (!id || !session?.symEncKey || !session?.symMacKey) return;
|
||||||
try {
|
try {
|
||||||
const encrypted = await getFolderById(authedFetch, id);
|
const encrypted = await getFolderById(authedFetch, id);
|
||||||
upsertEncryptedFolder(encrypted);
|
upsertEncryptedFolder(encrypted, revisionStamp);
|
||||||
const result = await decryptVaultCore({
|
const result = await decryptVaultCore({
|
||||||
folders: [encrypted],
|
folders: [encrypted],
|
||||||
ciphers: [],
|
ciphers: [],
|
||||||
@@ -1477,12 +1502,12 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function upsertSendFromNotification(sendId: string): Promise<void> {
|
async function upsertSendFromNotification(sendId: string, revisionStamp?: number | null): Promise<void> {
|
||||||
const id = String(sendId || '').trim();
|
const id = String(sendId || '').trim();
|
||||||
if (!id || !session?.symEncKey || !session?.symMacKey) return;
|
if (!id || !session?.symEncKey || !session?.symMacKey) return;
|
||||||
try {
|
try {
|
||||||
const encrypted = await getSendById(authedFetch, id);
|
const encrypted = await getSendById(authedFetch, id);
|
||||||
upsertEncryptedSend(encrypted);
|
upsertEncryptedSend(encrypted, revisionStamp);
|
||||||
const sends = await decryptSends({
|
const sends = await decryptSends({
|
||||||
sends: [encrypted],
|
sends: [encrypted],
|
||||||
symEncKeyB64: session.symEncKey,
|
symEncKeyB64: session.symEncKey,
|
||||||
@@ -1576,11 +1601,18 @@ export default function App() {
|
|||||||
const frames = parseSignalRTextFrames(event.data);
|
const frames = parseSignalRTextFrames(event.data);
|
||||||
for (const frame of frames) {
|
for (const frame of frames) {
|
||||||
if (frame.type !== 1 || frame.target !== 'ReceiveMessage') continue;
|
if (frame.type !== 1 || frame.target !== 'ReceiveMessage') continue;
|
||||||
const updateType = Number(frame.arguments?.[0]?.Type || 0);
|
const message = frame.arguments?.[0] as Record<string, unknown> | undefined;
|
||||||
const contextId = String(frame.arguments?.[0]?.ContextId || '').trim();
|
const updateType = Number(message?.Type || 0);
|
||||||
const payload = frame.arguments?.[0]?.Payload;
|
const contextId = String(message?.ContextId || '').trim();
|
||||||
|
const payload = message?.Payload;
|
||||||
const payloadRecord = payload && typeof payload === 'object' ? payload as Record<string, unknown> : null;
|
const payloadRecord = payload && typeof payload === 'object' ? payload as Record<string, unknown> : null;
|
||||||
const resourceId = String(payloadRecord?.Id || payloadRecord?.id || '').trim();
|
const resourceId = String(payloadRecord?.Id || payloadRecord?.id || '').trim();
|
||||||
|
const revisionStamp = revisionStampFromIso(
|
||||||
|
payloadRecord?.RevisionDate
|
||||||
|
|| payloadRecord?.revisionDate
|
||||||
|
|| message?.Date
|
||||||
|
|| message?.date
|
||||||
|
);
|
||||||
if (updateType === SIGNALR_UPDATE_TYPE_LOG_OUT) {
|
if (updateType === SIGNALR_UPDATE_TYPE_LOG_OUT) {
|
||||||
logoutNow();
|
logoutNow();
|
||||||
return;
|
return;
|
||||||
@@ -1590,7 +1622,6 @@ export default function App() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (updateType === SIGNALR_UPDATE_TYPE_BACKUP_RESTORE_PROGRESS) {
|
if (updateType === SIGNALR_UPDATE_TYPE_BACKUP_RESTORE_PROGRESS) {
|
||||||
const payload = frame.arguments?.[0]?.Payload;
|
|
||||||
if (isBackupProgressDetail(payload)) dispatchBackupProgress(payload);
|
if (isBackupProgressDetail(payload)) dispatchBackupProgress(payload);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1606,27 +1637,27 @@ export default function App() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ((updateType === SIGNALR_UPDATE_TYPE_SYNC_CIPHER_CREATE || updateType === SIGNALR_UPDATE_TYPE_SYNC_CIPHER_UPDATE) && resourceId) {
|
if ((updateType === SIGNALR_UPDATE_TYPE_SYNC_CIPHER_CREATE || updateType === SIGNALR_UPDATE_TYPE_SYNC_CIPHER_UPDATE) && resourceId) {
|
||||||
void upsertCipherFromNotification(resourceId);
|
void upsertCipherFromNotification(resourceId, revisionStamp);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (updateType === SIGNALR_UPDATE_TYPE_SYNC_CIPHER_DELETE && resourceId) {
|
if (updateType === SIGNALR_UPDATE_TYPE_SYNC_CIPHER_DELETE && resourceId) {
|
||||||
deleteCipherLocally(resourceId);
|
deleteCipherLocally(resourceId, revisionStamp);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ((updateType === SIGNALR_UPDATE_TYPE_SYNC_FOLDER_CREATE || updateType === SIGNALR_UPDATE_TYPE_SYNC_FOLDER_UPDATE) && resourceId) {
|
if ((updateType === SIGNALR_UPDATE_TYPE_SYNC_FOLDER_CREATE || updateType === SIGNALR_UPDATE_TYPE_SYNC_FOLDER_UPDATE) && resourceId) {
|
||||||
void upsertFolderFromNotification(resourceId);
|
void upsertFolderFromNotification(resourceId, revisionStamp);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (updateType === SIGNALR_UPDATE_TYPE_SYNC_FOLDER_DELETE && resourceId) {
|
if (updateType === SIGNALR_UPDATE_TYPE_SYNC_FOLDER_DELETE && resourceId) {
|
||||||
deleteFolderLocally(resourceId);
|
deleteFolderLocally(resourceId, revisionStamp);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ((updateType === SIGNALR_UPDATE_TYPE_SYNC_SEND_CREATE || updateType === SIGNALR_UPDATE_TYPE_SYNC_SEND_UPDATE) && resourceId) {
|
if ((updateType === SIGNALR_UPDATE_TYPE_SYNC_SEND_CREATE || updateType === SIGNALR_UPDATE_TYPE_SYNC_SEND_UPDATE) && resourceId) {
|
||||||
void upsertSendFromNotification(resourceId);
|
void upsertSendFromNotification(resourceId, revisionStamp);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (updateType === SIGNALR_UPDATE_TYPE_SYNC_SEND_DELETE && resourceId) {
|
if (updateType === SIGNALR_UPDATE_TYPE_SYNC_SEND_DELETE && resourceId) {
|
||||||
deleteSendLocally(resourceId);
|
deleteSendLocally(resourceId, revisionStamp);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (updateType === SIGNALR_UPDATE_TYPE_SYNC_VAULT) continue;
|
if (updateType === SIGNALR_UPDATE_TYPE_SYNC_VAULT) continue;
|
||||||
@@ -1688,8 +1719,33 @@ export default function App() {
|
|||||||
},
|
},
|
||||||
refetchSends: refetchSendsFromVaultCore,
|
refetchSends: refetchSendsFromVaultCore,
|
||||||
onNotify: pushToast,
|
onNotify: pushToast,
|
||||||
|
patchEncryptedCiphers: (updater) => {
|
||||||
|
patchVaultCoreSnapshot((snapshot) => ({
|
||||||
|
...snapshot,
|
||||||
|
ciphers: updater(snapshot.ciphers),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
patchEncryptedFolders: (updater) => {
|
||||||
|
patchVaultCoreSnapshot((snapshot) => ({
|
||||||
|
...snapshot,
|
||||||
|
folders: updater(snapshot.folders),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
patchEncryptedSends: (updater) => {
|
||||||
|
let nextSends: Send[] = [];
|
||||||
|
patchVaultCoreSnapshot((snapshot) => {
|
||||||
|
nextSends = updater(snapshot.sends);
|
||||||
|
return {
|
||||||
|
...snapshot,
|
||||||
|
sends: nextSends,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
queryClient.setQueryData(sendsQueryKey, nextSends);
|
||||||
|
},
|
||||||
patchDecryptedCiphers: setDecryptedCiphers,
|
patchDecryptedCiphers: setDecryptedCiphers,
|
||||||
patchDecryptedFolders: setDecryptedFolders,
|
patchDecryptedFolders: setDecryptedFolders,
|
||||||
|
patchDecryptedSends: setDecryptedSends,
|
||||||
|
refreshVaultRevisionStamp: refreshVaultCoreRevisionStamp,
|
||||||
});
|
});
|
||||||
const accountSecurityActions = useAccountSecurityActions({
|
const accountSecurityActions = useAccountSecurityActions({
|
||||||
authedFetch,
|
authedFetch,
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
downloadCipherAttachmentDecrypted,
|
downloadCipherAttachmentDecrypted,
|
||||||
encryptFolderImportName,
|
encryptFolderImportName,
|
||||||
getAttachmentDownloadInfo,
|
getAttachmentDownloadInfo,
|
||||||
|
getCipherById,
|
||||||
importCiphers,
|
importCiphers,
|
||||||
permanentDeleteCipher,
|
permanentDeleteCipher,
|
||||||
type CiphersImportPayload,
|
type CiphersImportPayload,
|
||||||
@@ -69,8 +70,13 @@ interface UseVaultSendActionsOptions {
|
|||||||
refetchFolders: () => Promise<{ data?: VaultFolder[] | undefined } | unknown>;
|
refetchFolders: () => Promise<{ data?: VaultFolder[] | undefined } | unknown>;
|
||||||
refetchSends: () => Promise<unknown>;
|
refetchSends: () => Promise<unknown>;
|
||||||
onNotify: Notify;
|
onNotify: Notify;
|
||||||
|
patchEncryptedCiphers: (updater: (prev: Cipher[]) => Cipher[]) => void;
|
||||||
|
patchEncryptedFolders: (updater: (prev: VaultFolder[]) => VaultFolder[]) => void;
|
||||||
|
patchEncryptedSends: (updater: (prev: Send[]) => Send[]) => void;
|
||||||
patchDecryptedCiphers: (updater: (prev: Cipher[]) => Cipher[]) => void;
|
patchDecryptedCiphers: (updater: (prev: Cipher[]) => Cipher[]) => void;
|
||||||
patchDecryptedFolders: (updater: (prev: VaultFolder[]) => VaultFolder[]) => void;
|
patchDecryptedFolders: (updater: (prev: VaultFolder[]) => VaultFolder[]) => void;
|
||||||
|
patchDecryptedSends: (updater: (prev: Send[]) => Send[]) => void;
|
||||||
|
refreshVaultRevisionStamp: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractImportIdMaps(cipherMap: ImportedCipherMapEntry[] | null) {
|
function extractImportIdMaps(cipherMap: ImportedCipherMapEntry[] | null) {
|
||||||
@@ -288,8 +294,13 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
refetchFolders,
|
refetchFolders,
|
||||||
refetchSends,
|
refetchSends,
|
||||||
onNotify,
|
onNotify,
|
||||||
|
patchEncryptedCiphers,
|
||||||
|
patchEncryptedFolders,
|
||||||
|
patchEncryptedSends,
|
||||||
patchDecryptedCiphers,
|
patchDecryptedCiphers,
|
||||||
patchDecryptedFolders,
|
patchDecryptedFolders,
|
||||||
|
patchDecryptedSends,
|
||||||
|
refreshVaultRevisionStamp,
|
||||||
} = options;
|
} = options;
|
||||||
const [downloadingAttachmentKey, setDownloadingAttachmentKey] = useState('');
|
const [downloadingAttachmentKey, setDownloadingAttachmentKey] = useState('');
|
||||||
const [attachmentDownloadPercent, setAttachmentDownloadPercent] = useState<number | null>(null);
|
const [attachmentDownloadPercent, setAttachmentDownloadPercent] = useState<number | null>(null);
|
||||||
@@ -308,21 +319,20 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
throw new Error(t('txt_offline_vault_readonly'));
|
throw new Error(t('txt_offline_vault_readonly'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncVaultCoreInBackground = (options?: { includeFolders?: boolean }) => {
|
|
||||||
const tasks: Promise<unknown>[] = [Promise.resolve(refetchCiphers())];
|
|
||||||
if (options?.includeFolders) {
|
|
||||||
tasks.push(Promise.resolve(refetchFolders()));
|
|
||||||
}
|
|
||||||
void Promise.all(tasks).catch((err) => {
|
|
||||||
console.warn('Background vault sync failed:', err);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
async function decryptAndPatch(encrypted: Cipher) {
|
async function decryptAndPatch(encrypted: Cipher) {
|
||||||
if (!session?.symEncKey || !session?.symMacKey) {
|
if (!session?.symEncKey || !session?.symMacKey) {
|
||||||
await refetchCiphers();
|
await refetchCiphers();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
patchEncryptedCiphers((prev) => {
|
||||||
|
const idx = prev.findIndex((c) => c.id === encrypted.id);
|
||||||
|
if (idx >= 0) {
|
||||||
|
const next = [...prev];
|
||||||
|
next[idx] = encrypted;
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
return [encrypted, ...prev];
|
||||||
|
});
|
||||||
const encKey = base64ToBytes(session.symEncKey);
|
const encKey = base64ToBytes(session.symEncKey);
|
||||||
const macKey = base64ToBytes(session.symMacKey);
|
const macKey = base64ToBytes(session.symMacKey);
|
||||||
const decrypted = await decryptSingleCipher(encrypted, encKey, macKey);
|
const decrypted = await decryptSingleCipher(encrypted, encKey, macKey);
|
||||||
@@ -342,6 +352,7 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
await refetchCiphers();
|
await refetchCiphers();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
patchEncryptedCiphers((prev) => [encrypted, ...prev.filter((cipher) => cipher.id !== optimisticId && cipher.id !== encrypted.id)]);
|
||||||
const encKey = base64ToBytes(session.symEncKey);
|
const encKey = base64ToBytes(session.symEncKey);
|
||||||
const macKey = base64ToBytes(session.symMacKey);
|
const macKey = base64ToBytes(session.symMacKey);
|
||||||
const decrypted = await decryptSingleCipher(encrypted, encKey, macKey);
|
const decrypted = await decryptSingleCipher(encrypted, encKey, macKey);
|
||||||
@@ -352,12 +363,36 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeCipherFromState(id: string) {
|
function removeCipherFromState(id: string) {
|
||||||
|
patchEncryptedCiphers((prev) => prev.filter((c) => c.id !== id));
|
||||||
patchDecryptedCiphers((prev) => prev.filter((c) => c.id !== id));
|
patchDecryptedCiphers((prev) => prev.filter((c) => c.id !== id));
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchCipherBatch(ids: string[], updater: (cipher: Cipher) => Cipher | null) {
|
function patchCipherBatch(
|
||||||
|
ids: string[],
|
||||||
|
updater: (cipher: Cipher) => Cipher | null,
|
||||||
|
options?: { patchEncrypted?: boolean; patchDecrypted?: boolean }
|
||||||
|
) {
|
||||||
const idSet = new Set(ids.map((id) => String(id || '').trim()).filter(Boolean));
|
const idSet = new Set(ids.map((id) => String(id || '').trim()).filter(Boolean));
|
||||||
if (!idSet.size) return;
|
if (!idSet.size) return;
|
||||||
|
const shouldPatchEncrypted = options?.patchEncrypted !== false;
|
||||||
|
const shouldPatchDecrypted = options?.patchDecrypted !== false;
|
||||||
|
if (shouldPatchEncrypted) {
|
||||||
|
patchEncryptedCiphers((prev) => {
|
||||||
|
let changed = false;
|
||||||
|
const next: Cipher[] = [];
|
||||||
|
for (const cipher of prev) {
|
||||||
|
if (!idSet.has(cipher.id)) {
|
||||||
|
next.push(cipher);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const updated = updater(cipher);
|
||||||
|
changed = true;
|
||||||
|
if (updated) next.push(updated);
|
||||||
|
}
|
||||||
|
return changed ? next : prev;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (shouldPatchDecrypted) {
|
||||||
patchDecryptedCiphers((prev) => {
|
patchDecryptedCiphers((prev) => {
|
||||||
let changed = false;
|
let changed = false;
|
||||||
const next: Cipher[] = [];
|
const next: Cipher[] = [];
|
||||||
@@ -373,10 +408,25 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
return changed ? next : prev;
|
return changed ? next : prev;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function patchFolderBatch(ids: string[], updater: (folder: VaultFolder) => VaultFolder | null) {
|
function patchFolderBatch(ids: string[], updater: (folder: VaultFolder) => VaultFolder | null) {
|
||||||
const idSet = new Set(ids.map((id) => String(id || '').trim()).filter(Boolean));
|
const idSet = new Set(ids.map((id) => String(id || '').trim()).filter(Boolean));
|
||||||
if (!idSet.size) return;
|
if (!idSet.size) return;
|
||||||
|
patchEncryptedFolders((prev) => {
|
||||||
|
let changed = false;
|
||||||
|
const next: VaultFolder[] = [];
|
||||||
|
for (const folder of prev) {
|
||||||
|
if (!idSet.has(folder.id)) {
|
||||||
|
next.push(folder);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const updated = updater(folder);
|
||||||
|
changed = true;
|
||||||
|
if (updated) next.push(updated);
|
||||||
|
}
|
||||||
|
return changed ? next : prev;
|
||||||
|
});
|
||||||
patchDecryptedFolders((prev) => {
|
patchDecryptedFolders((prev) => {
|
||||||
let changed = false;
|
let changed = false;
|
||||||
const next: VaultFolder[] = [];
|
const next: VaultFolder[] = [];
|
||||||
@@ -393,6 +443,31 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function upsertEncryptedFolder(folder: VaultFolder) {
|
||||||
|
patchEncryptedFolders((prev) => {
|
||||||
|
const index = prev.findIndex((item) => item.id === folder.id);
|
||||||
|
if (index < 0) return [folder, ...prev];
|
||||||
|
const next = [...prev];
|
||||||
|
next[index] = folder;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsertSend(send: Send) {
|
||||||
|
patchEncryptedSends((prev) => {
|
||||||
|
const index = prev.findIndex((item) => item.id === send.id);
|
||||||
|
if (index < 0) return [send, ...prev];
|
||||||
|
const next = [...prev];
|
||||||
|
next[index] = send;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSend(id: string) {
|
||||||
|
patchEncryptedSends((prev) => prev.filter((send) => send.id !== id));
|
||||||
|
patchDecryptedSends((prev) => prev.filter((send) => send.id !== id));
|
||||||
|
}
|
||||||
|
|
||||||
const uploadImportedAttachments = async (
|
const uploadImportedAttachments = async (
|
||||||
attachments: ImportAttachmentFile[],
|
attachments: ImportAttachmentFile[],
|
||||||
idMaps: { byIndex: Map<number, string>; bySourceId: Map<string, string> }
|
idMaps: { byIndex: Map<number, string>; bySourceId: Map<string, string> }
|
||||||
@@ -468,8 +543,9 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
setAttachmentUploadPercent(0);
|
setAttachmentUploadPercent(0);
|
||||||
await uploadCipherAttachment(authedFetch, session, created.id, file, undefined, setAttachmentUploadPercent);
|
await uploadCipherAttachment(authedFetch, session, created.id, file, undefined, setAttachmentUploadPercent);
|
||||||
}
|
}
|
||||||
await decryptAndReplaceOptimistic(optimistic.id, created);
|
const finalCipher = attachments.length ? await getCipherById(authedFetch, created.id) : created;
|
||||||
syncVaultCoreInBackground({ includeFolders: !!draft.folderId || attachments.length > 0 });
|
await decryptAndReplaceOptimistic(optimistic.id, finalCipher);
|
||||||
|
void refreshVaultRevisionStamp();
|
||||||
onNotify('success', t('txt_item_created'));
|
onNotify('success', t('txt_item_created'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
patchDecryptedCiphers((prev) => prev.filter((cipher) => cipher.id !== optimistic.id));
|
patchDecryptedCiphers((prev) => prev.filter((cipher) => cipher.id !== optimistic.id));
|
||||||
@@ -511,7 +587,7 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
.filter((attachment) => !removedSet.has(String(attachment?.id || '').trim()))
|
.filter((attachment) => !removedSet.has(String(attachment?.id || '').trim()))
|
||||||
.map((attachment) => ({ ...attachment }));
|
.map((attachment) => ({ ...attachment }));
|
||||||
}
|
}
|
||||||
patchCipherBatch([cipher.id], () => optimistic);
|
patchCipherBatch([cipher.id], () => optimistic, { patchEncrypted: false });
|
||||||
try {
|
try {
|
||||||
const updated = await updateCipher(authedFetch, session, cipher, draft);
|
const updated = await updateCipher(authedFetch, session, cipher, draft);
|
||||||
for (const attachmentId of removeAttachmentIds) {
|
for (const attachmentId of removeAttachmentIds) {
|
||||||
@@ -524,16 +600,14 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
setAttachmentUploadPercent(0);
|
setAttachmentUploadPercent(0);
|
||||||
await uploadCipherAttachment(authedFetch, session, cipher.id, file, cipher, setAttachmentUploadPercent);
|
await uploadCipherAttachment(authedFetch, session, cipher.id, file, cipher, setAttachmentUploadPercent);
|
||||||
}
|
}
|
||||||
await decryptAndPatch(updated);
|
const finalCipher = addFiles.length || removeAttachmentIds.length
|
||||||
syncVaultCoreInBackground({
|
? await getCipherById(authedFetch, cipher.id)
|
||||||
includeFolders:
|
: updated;
|
||||||
draft.folderId !== (cipher.folderId || '')
|
await decryptAndPatch(finalCipher);
|
||||||
|| addFiles.length > 0
|
void refreshVaultRevisionStamp();
|
||||||
|| removeAttachmentIds.length > 0,
|
|
||||||
});
|
|
||||||
onNotify('success', t('txt_item_updated'));
|
onNotify('success', t('txt_item_updated'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
patchCipherBatch([cipher.id], () => previousCipher);
|
patchCipherBatch([cipher.id], () => previousCipher, { patchEncrypted: false });
|
||||||
onNotify('error', error instanceof Error ? error.message : t('txt_update_item_failed'));
|
onNotify('error', error instanceof Error ? error.message : t('txt_update_item_failed'));
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -572,7 +646,7 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
try {
|
try {
|
||||||
await permanentDeleteCipher(authedFetch, cipher.id);
|
await permanentDeleteCipher(authedFetch, cipher.id);
|
||||||
patchCipherBatch([cipher.id], () => null);
|
patchCipherBatch([cipher.id], () => null);
|
||||||
syncVaultCoreInBackground({ includeFolders: true });
|
void refreshVaultRevisionStamp();
|
||||||
onNotify('success', t('txt_item_deleted_permanently'));
|
onNotify('success', t('txt_item_deleted_permanently'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onNotify('error', error instanceof Error ? error.message : t('txt_permanent_delete_item_failed'));
|
onNotify('error', error instanceof Error ? error.message : t('txt_permanent_delete_item_failed'));
|
||||||
@@ -585,10 +659,10 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
try {
|
try {
|
||||||
const deleted = await deleteCipher(authedFetch, cipher.id);
|
const deleted = await deleteCipher(authedFetch, cipher.id);
|
||||||
await decryptAndPatch(deleted);
|
await decryptAndPatch(deleted);
|
||||||
syncVaultCoreInBackground({ includeFolders: true });
|
void refreshVaultRevisionStamp();
|
||||||
onNotify('success', t('txt_item_deleted'));
|
onNotify('success', t('txt_item_deleted'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
patchCipherBatch([cipher.id], () => previousCipher);
|
patchCipherBatch([cipher.id], () => previousCipher, { patchEncrypted: false });
|
||||||
onNotify('error', error instanceof Error ? error.message : t('txt_delete_item_failed'));
|
onNotify('error', error instanceof Error ? error.message : t('txt_delete_item_failed'));
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -607,10 +681,10 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
try {
|
try {
|
||||||
const archived = await archiveCipher(authedFetch, cipher.id);
|
const archived = await archiveCipher(authedFetch, cipher.id);
|
||||||
await decryptAndPatch(archived);
|
await decryptAndPatch(archived);
|
||||||
syncVaultCoreInBackground({ includeFolders: true });
|
void refreshVaultRevisionStamp();
|
||||||
onNotify('success', t('txt_item_archived'));
|
onNotify('success', t('txt_item_archived'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
patchCipherBatch([cipher.id], () => previousCipher);
|
patchCipherBatch([cipher.id], () => previousCipher, { patchEncrypted: false });
|
||||||
onNotify('error', error instanceof Error ? error.message : t('txt_archive_item_failed'));
|
onNotify('error', error instanceof Error ? error.message : t('txt_archive_item_failed'));
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -629,10 +703,10 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
try {
|
try {
|
||||||
const unarchived = await unarchiveCipher(authedFetch, cipher.id);
|
const unarchived = await unarchiveCipher(authedFetch, cipher.id);
|
||||||
await decryptAndPatch(unarchived);
|
await decryptAndPatch(unarchived);
|
||||||
syncVaultCoreInBackground({ includeFolders: true });
|
void refreshVaultRevisionStamp();
|
||||||
onNotify('success', t('txt_item_unarchived'));
|
onNotify('success', t('txt_item_unarchived'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
patchCipherBatch([cipher.id], () => previousCipher);
|
patchCipherBatch([cipher.id], () => previousCipher, { patchEncrypted: false });
|
||||||
onNotify('error', error instanceof Error ? error.message : t('txt_unarchive_item_failed'));
|
onNotify('error', error instanceof Error ? error.message : t('txt_unarchive_item_failed'));
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -649,7 +723,7 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
await bulkDeleteCiphers(authedFetch, ids);
|
await bulkDeleteCiphers(authedFetch, ids);
|
||||||
const deletedDate = new Date().toISOString();
|
const deletedDate = new Date().toISOString();
|
||||||
patchCipherBatch(ids, (cipher) => ({ ...cipher, deletedDate, archivedDate: null }));
|
patchCipherBatch(ids, (cipher) => ({ ...cipher, deletedDate, archivedDate: null }));
|
||||||
syncVaultCoreInBackground({ includeFolders: true });
|
void refreshVaultRevisionStamp();
|
||||||
onNotify('success', t('txt_deleted_selected_items'));
|
onNotify('success', t('txt_deleted_selected_items'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onNotify('error', error instanceof Error ? error.message : t('txt_bulk_delete_failed'));
|
onNotify('error', error instanceof Error ? error.message : t('txt_bulk_delete_failed'));
|
||||||
@@ -668,7 +742,7 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
await bulkArchiveCiphers(authedFetch, ids);
|
await bulkArchiveCiphers(authedFetch, ids);
|
||||||
const archivedDate = new Date().toISOString();
|
const archivedDate = new Date().toISOString();
|
||||||
patchCipherBatch(ids, (cipher) => ({ ...cipher, archivedDate, deletedDate: null }));
|
patchCipherBatch(ids, (cipher) => ({ ...cipher, archivedDate, deletedDate: null }));
|
||||||
syncVaultCoreInBackground({ includeFolders: true });
|
void refreshVaultRevisionStamp();
|
||||||
onNotify('success', t('txt_archived_selected_items'));
|
onNotify('success', t('txt_archived_selected_items'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onNotify('error', error instanceof Error ? error.message : t('txt_bulk_archive_failed'));
|
onNotify('error', error instanceof Error ? error.message : t('txt_bulk_archive_failed'));
|
||||||
@@ -686,7 +760,7 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
try {
|
try {
|
||||||
await bulkUnarchiveCiphers(authedFetch, ids);
|
await bulkUnarchiveCiphers(authedFetch, ids);
|
||||||
patchCipherBatch(ids, (cipher) => ({ ...cipher, archivedDate: null }));
|
patchCipherBatch(ids, (cipher) => ({ ...cipher, archivedDate: null }));
|
||||||
syncVaultCoreInBackground({ includeFolders: true });
|
void refreshVaultRevisionStamp();
|
||||||
onNotify('success', t('txt_unarchived_selected_items'));
|
onNotify('success', t('txt_unarchived_selected_items'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onNotify('error', error instanceof Error ? error.message : t('txt_bulk_unarchive_failed'));
|
onNotify('error', error instanceof Error ? error.message : t('txt_bulk_unarchive_failed'));
|
||||||
@@ -704,7 +778,7 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
try {
|
try {
|
||||||
await bulkMoveCiphers(authedFetch, ids, folderId);
|
await bulkMoveCiphers(authedFetch, ids, folderId);
|
||||||
patchCipherBatch(ids, (cipher) => ({ ...cipher, folderId }));
|
patchCipherBatch(ids, (cipher) => ({ ...cipher, folderId }));
|
||||||
syncVaultCoreInBackground({ includeFolders: true });
|
void refreshVaultRevisionStamp();
|
||||||
onNotify('success', t('txt_moved_selected_items'));
|
onNotify('success', t('txt_moved_selected_items'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onNotify('error', error instanceof Error ? error.message : t('txt_bulk_move_failed'));
|
onNotify('error', error instanceof Error ? error.message : t('txt_bulk_move_failed'));
|
||||||
@@ -727,15 +801,18 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
try {
|
try {
|
||||||
if (!session) throw new Error(t('txt_vault_key_unavailable'));
|
if (!session) throw new Error(t('txt_vault_key_unavailable'));
|
||||||
const created = await createFolder(authedFetch, session, folderName);
|
const created = await createFolder(authedFetch, session, folderName);
|
||||||
|
upsertEncryptedFolder(created);
|
||||||
patchDecryptedFolders((prev) => [
|
patchDecryptedFolders((prev) => [
|
||||||
{
|
{
|
||||||
id: created.id,
|
id: created.id,
|
||||||
name: created.name || folderName,
|
name: created.name || folderName,
|
||||||
decName: folderName,
|
decName: folderName,
|
||||||
|
revisionDate: created.revisionDate,
|
||||||
|
creationDate: created.creationDate,
|
||||||
},
|
},
|
||||||
...prev,
|
...prev,
|
||||||
]);
|
]);
|
||||||
syncVaultCoreInBackground({ includeFolders: true });
|
void refreshVaultRevisionStamp();
|
||||||
onNotify('success', t('txt_folder_created'));
|
onNotify('success', t('txt_folder_created'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onNotify('error', error instanceof Error ? error.message : t('txt_create_folder_failed'));
|
onNotify('error', error instanceof Error ? error.message : t('txt_create_folder_failed'));
|
||||||
@@ -758,8 +835,9 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
try {
|
try {
|
||||||
await deleteFolder(authedFetch, id);
|
await deleteFolder(authedFetch, id);
|
||||||
patchFolderBatch([id], () => null);
|
patchFolderBatch([id], () => null);
|
||||||
|
patchEncryptedCiphers((prev) => prev.map((cipher) => (cipher.folderId === id ? { ...cipher, folderId: null } : cipher)));
|
||||||
patchDecryptedCiphers((prev) => prev.map((cipher) => (cipher.folderId === id ? { ...cipher, folderId: null } : cipher)));
|
patchDecryptedCiphers((prev) => prev.map((cipher) => (cipher.folderId === id ? { ...cipher, folderId: null } : cipher)));
|
||||||
syncVaultCoreInBackground({ includeFolders: true });
|
void refreshVaultRevisionStamp();
|
||||||
onNotify('success', t('txt_folder_deleted'));
|
onNotify('success', t('txt_folder_deleted'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onNotify('error', error instanceof Error ? error.message : t('txt_delete_folder_failed'));
|
onNotify('error', error instanceof Error ? error.message : t('txt_delete_folder_failed'));
|
||||||
@@ -786,9 +864,14 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (!session) throw new Error(t('txt_vault_key_unavailable'));
|
if (!session) throw new Error(t('txt_vault_key_unavailable'));
|
||||||
await updateFolder(authedFetch, session, id, nextName);
|
const updated = await updateFolder(authedFetch, session, id, nextName);
|
||||||
patchFolderBatch([id], (folder) => ({ ...folder, decName: nextName }));
|
upsertEncryptedFolder(updated);
|
||||||
syncVaultCoreInBackground({ includeFolders: true });
|
patchDecryptedFolders((prev) => prev.map((folder) => (
|
||||||
|
folder.id === id
|
||||||
|
? { ...folder, name: updated.name || folder.name, decName: nextName, revisionDate: updated.revisionDate }
|
||||||
|
: folder
|
||||||
|
)));
|
||||||
|
void refreshVaultRevisionStamp();
|
||||||
onNotify('success', t('txt_folder_updated'));
|
onNotify('success', t('txt_folder_updated'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onNotify('error', error instanceof Error ? error.message : t('txt_update_folder_failed'));
|
onNotify('error', error instanceof Error ? error.message : t('txt_update_folder_failed'));
|
||||||
@@ -806,7 +889,7 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
try {
|
try {
|
||||||
await bulkRestoreCiphers(authedFetch, ids);
|
await bulkRestoreCiphers(authedFetch, ids);
|
||||||
patchCipherBatch(ids, (cipher) => ({ ...cipher, deletedDate: null }));
|
patchCipherBatch(ids, (cipher) => ({ ...cipher, deletedDate: null }));
|
||||||
syncVaultCoreInBackground({ includeFolders: true });
|
void refreshVaultRevisionStamp();
|
||||||
onNotify('success', t('txt_restored_selected_items'));
|
onNotify('success', t('txt_restored_selected_items'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onNotify('error', error instanceof Error ? error.message : t('txt_bulk_restore_failed'));
|
onNotify('error', error instanceof Error ? error.message : t('txt_bulk_restore_failed'));
|
||||||
@@ -824,7 +907,7 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
try {
|
try {
|
||||||
await bulkPermanentDeleteCiphers(authedFetch, ids);
|
await bulkPermanentDeleteCiphers(authedFetch, ids);
|
||||||
patchCipherBatch(ids, () => null);
|
patchCipherBatch(ids, () => null);
|
||||||
syncVaultCoreInBackground({ includeFolders: true });
|
void refreshVaultRevisionStamp();
|
||||||
onNotify('success', t('txt_deleted_selected_items_permanently'));
|
onNotify('success', t('txt_deleted_selected_items_permanently'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onNotify('error', error instanceof Error ? error.message : t('txt_bulk_permanent_delete_failed'));
|
onNotify('error', error instanceof Error ? error.message : t('txt_bulk_permanent_delete_failed'));
|
||||||
@@ -844,9 +927,11 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
try {
|
try {
|
||||||
await bulkDeleteFolders(authedFetch, ids);
|
await bulkDeleteFolders(authedFetch, ids);
|
||||||
const removedIds = new Set(ids);
|
const removedIds = new Set(ids);
|
||||||
|
patchEncryptedFolders((prev) => prev.filter((folder) => !removedIds.has(folder.id)));
|
||||||
|
patchEncryptedCiphers((prev) => prev.map((cipher) => (cipher.folderId && removedIds.has(cipher.folderId) ? { ...cipher, folderId: null } : cipher)));
|
||||||
patchDecryptedFolders((prev) => prev.filter((folder) => !removedIds.has(folder.id)));
|
patchDecryptedFolders((prev) => prev.filter((folder) => !removedIds.has(folder.id)));
|
||||||
patchDecryptedCiphers((prev) => prev.map((cipher) => (cipher.folderId && removedIds.has(cipher.folderId) ? { ...cipher, folderId: null } : cipher)));
|
patchDecryptedCiphers((prev) => prev.map((cipher) => (cipher.folderId && removedIds.has(cipher.folderId) ? { ...cipher, folderId: null } : cipher)));
|
||||||
syncVaultCoreInBackground({ includeFolders: true });
|
void refreshVaultRevisionStamp();
|
||||||
onNotify('success', t('txt_folders_deleted'));
|
onNotify('success', t('txt_folders_deleted'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onNotify('error', error instanceof Error ? error.message : t('txt_delete_all_folders_failed'));
|
onNotify('error', error instanceof Error ? error.message : t('txt_delete_all_folders_failed'));
|
||||||
@@ -874,7 +959,8 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
setSendUploadPercent(0);
|
setSendUploadPercent(0);
|
||||||
}
|
}
|
||||||
const created = await createSend(authedFetch, session, draft, fileName ? setSendUploadPercent : undefined);
|
const created = await createSend(authedFetch, session, draft, fileName ? setSendUploadPercent : undefined);
|
||||||
await refetchSends();
|
upsertSend(created);
|
||||||
|
void refreshVaultRevisionStamp();
|
||||||
if (autoCopyLink && created.key && session.symEncKey && session.symMacKey) {
|
if (autoCopyLink && created.key && session.symEncKey && session.symMacKey) {
|
||||||
const keyPart = await buildSendShareKey(created.key, session.symEncKey, session.symMacKey);
|
const keyPart = await buildSendShareKey(created.key, session.symEncKey, session.symMacKey);
|
||||||
const shareUrl = buildPublicSendUrl(window.location.origin, created.accessId, keyPart);
|
const shareUrl = buildPublicSendUrl(window.location.origin, created.accessId, keyPart);
|
||||||
@@ -900,7 +986,8 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const updated = await updateSend(authedFetch, session, send, draft);
|
const updated = await updateSend(authedFetch, session, send, draft);
|
||||||
await refetchSends();
|
upsertSend(updated);
|
||||||
|
void refreshVaultRevisionStamp();
|
||||||
if (autoCopyLink && updated.key && session.symEncKey && session.symMacKey) {
|
if (autoCopyLink && updated.key && session.symEncKey && session.symMacKey) {
|
||||||
const keyPart = await buildSendShareKey(updated.key, session.symEncKey, session.symMacKey);
|
const keyPart = await buildSendShareKey(updated.key, session.symEncKey, session.symMacKey);
|
||||||
const shareUrl = buildPublicSendUrl(window.location.origin, updated.accessId, keyPart);
|
const shareUrl = buildPublicSendUrl(window.location.origin, updated.accessId, keyPart);
|
||||||
@@ -922,7 +1009,8 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await deleteSend(authedFetch, send.id);
|
await deleteSend(authedFetch, send.id);
|
||||||
await refetchSends();
|
removeSend(send.id);
|
||||||
|
void refreshVaultRevisionStamp();
|
||||||
onNotify('success', t('txt_send_deleted'));
|
onNotify('success', t('txt_send_deleted'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onNotify('error', error instanceof Error ? error.message : t('txt_delete_send_failed'));
|
onNotify('error', error instanceof Error ? error.message : t('txt_delete_send_failed'));
|
||||||
@@ -939,7 +1027,10 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await bulkDeleteSends(authedFetch, ids);
|
await bulkDeleteSends(authedFetch, ids);
|
||||||
await refetchSends();
|
const idSet = new Set(ids.map((id) => String(id || '').trim()).filter(Boolean));
|
||||||
|
patchEncryptedSends((prev) => prev.filter((send) => !idSet.has(send.id)));
|
||||||
|
patchDecryptedSends((prev) => prev.filter((send) => !idSet.has(send.id)));
|
||||||
|
void refreshVaultRevisionStamp();
|
||||||
onNotify('success', t('txt_deleted_selected_sends'));
|
onNotify('success', t('txt_deleted_selected_sends'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onNotify('error', error instanceof Error ? error.message : t('txt_bulk_delete_sends_failed'));
|
onNotify('error', error instanceof Error ? error.message : t('txt_bulk_delete_sends_failed'));
|
||||||
@@ -1299,10 +1390,17 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
encryptedFolders,
|
encryptedFolders,
|
||||||
importAuthedFetch,
|
importAuthedFetch,
|
||||||
onNotify,
|
onNotify,
|
||||||
|
patchDecryptedCiphers,
|
||||||
|
patchDecryptedFolders,
|
||||||
|
patchDecryptedSends,
|
||||||
|
patchEncryptedCiphers,
|
||||||
|
patchEncryptedFolders,
|
||||||
|
patchEncryptedSends,
|
||||||
profile,
|
profile,
|
||||||
refetchCiphers,
|
refetchCiphers,
|
||||||
refetchFolders,
|
refetchFolders,
|
||||||
refetchSends,
|
refetchSends,
|
||||||
|
refreshVaultRevisionStamp,
|
||||||
session,
|
session,
|
||||||
sendUploadPercent,
|
sendUploadPercent,
|
||||||
uploadingAttachmentName,
|
uploadingAttachmentName,
|
||||||
|
|||||||
@@ -51,6 +51,29 @@ export async function invalidateVaultCoreSyncSnapshot(cacheKey: string): Promise
|
|||||||
await clearCachedVaultCoreSnapshot(normalizedKey);
|
await clearCachedVaultCoreSnapshot(normalizedKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function saveVaultCoreSyncSnapshot(
|
||||||
|
cacheKey: string,
|
||||||
|
snapshot: VaultCoreSnapshot,
|
||||||
|
revisionStamp?: number | null
|
||||||
|
): Promise<void> {
|
||||||
|
const normalizedKey = String(cacheKey || '').trim();
|
||||||
|
if (!normalizedKey) return;
|
||||||
|
|
||||||
|
const normalizedSnapshot = normalizeCachedSnapshot(snapshot);
|
||||||
|
const currentMemory = memoryVaultCoreCache.get(normalizedKey);
|
||||||
|
let nextRevisionStamp = Number(revisionStamp);
|
||||||
|
if (!Number.isFinite(nextRevisionStamp) || nextRevisionStamp <= 0) {
|
||||||
|
const cached = await loadCachedVaultCoreSnapshot(normalizedKey);
|
||||||
|
nextRevisionStamp = currentMemory?.revisionStamp || cached?.revisionStamp || Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
memoryVaultCoreCache.set(normalizedKey, {
|
||||||
|
revisionStamp: nextRevisionStamp,
|
||||||
|
snapshot: normalizedSnapshot,
|
||||||
|
});
|
||||||
|
await saveCachedVaultCoreSnapshot(normalizedKey, nextRevisionStamp, normalizedSnapshot);
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadVaultCoreSyncSnapshot(authedFetch: AuthedFetch, cacheKey: string): Promise<VaultCoreSnapshot> {
|
export async function loadVaultCoreSyncSnapshot(authedFetch: AuthedFetch, cacheKey: string): Promise<VaultCoreSnapshot> {
|
||||||
const normalizedKey = String(cacheKey || '').trim();
|
const normalizedKey = String(cacheKey || '').trim();
|
||||||
if (!normalizedKey) return { ciphers: [], folders: [], sends: [] };
|
if (!normalizedKey) return { ciphers: [], folders: [], sends: [] };
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export async function createFolder(
|
|||||||
authedFetch: AuthedFetch,
|
authedFetch: AuthedFetch,
|
||||||
session: SessionState,
|
session: SessionState,
|
||||||
name: string
|
name: string
|
||||||
): Promise<{ id: string; name?: string | null }> {
|
): Promise<Folder> {
|
||||||
if (!session.symEncKey || !session.symMacKey) throw new Error('Vault key unavailable');
|
if (!session.symEncKey || !session.symMacKey) throw new Error('Vault key unavailable');
|
||||||
const enc = base64ToBytes(session.symEncKey);
|
const enc = base64ToBytes(session.symEncKey);
|
||||||
const mac = base64ToBytes(session.symMacKey);
|
const mac = base64ToBytes(session.symMacKey);
|
||||||
@@ -54,9 +54,9 @@ export async function createFolder(
|
|||||||
body: JSON.stringify({ name: encryptedName }),
|
body: JSON.stringify({ name: encryptedName }),
|
||||||
});
|
});
|
||||||
if (!resp.ok) throw new Error('Create folder failed');
|
if (!resp.ok) throw new Error('Create folder failed');
|
||||||
const body = await parseJson<{ id?: string; name?: string | null }>(resp);
|
const body = await parseJson<Folder>(resp);
|
||||||
if (!body?.id) throw new Error('Create folder failed');
|
if (!body?.id) throw new Error('Create folder failed');
|
||||||
return { id: body.id, name: body.name ?? null };
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function encryptFolderImportName(session: SessionState, name: string): Promise<string> {
|
export async function encryptFolderImportName(session: SessionState, name: string): Promise<string> {
|
||||||
@@ -92,7 +92,7 @@ export async function updateFolder(
|
|||||||
session: SessionState,
|
session: SessionState,
|
||||||
folderId: string,
|
folderId: string,
|
||||||
name: string
|
name: string
|
||||||
): Promise<void> {
|
): Promise<Folder> {
|
||||||
const id = String(folderId || '').trim();
|
const id = String(folderId || '').trim();
|
||||||
if (!id) throw new Error('Folder id is required');
|
if (!id) throw new Error('Folder id is required');
|
||||||
if (!session.symEncKey || !session.symMacKey) throw new Error('Vault key unavailable');
|
if (!session.symEncKey || !session.symMacKey) throw new Error('Vault key unavailable');
|
||||||
@@ -105,6 +105,9 @@ export async function updateFolder(
|
|||||||
body: JSON.stringify({ name: encryptedName }),
|
body: JSON.stringify({ name: encryptedName }),
|
||||||
});
|
});
|
||||||
if (!resp.ok) throw new Error('Update folder failed');
|
if (!resp.ok) throw new Error('Update folder failed');
|
||||||
|
const body = await parseJson<Folder>(resp);
|
||||||
|
if (!body?.id) throw new Error('Update folder failed');
|
||||||
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCiphers(authedFetch: AuthedFetch, cacheKey: string): Promise<Cipher[]> {
|
export async function getCiphers(authedFetch: AuthedFetch, cacheKey: string): Promise<Cipher[]> {
|
||||||
|
|||||||
Reference in New Issue
Block a user