mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
101 lines
4.7 KiB
TypeScript
101 lines
4.7 KiB
TypeScript
import { apiFetch } from '@/lib/browser-navigation';
|
|
import { IS_LITE } from '@/lib/lite';
|
|
import { generateAccountId, getMaxAccounts } from '@/lib/account-utils';
|
|
import { useAccountStore } from '@/stores/account-store';
|
|
import { useAuthStore } from '@/stores/auth-store';
|
|
import type { ArchiveEnvelope, ArchiveOwner, ArchivedAccount } from './account-archive';
|
|
|
|
async function request(owner: ArchiveOwner, init: RequestInit = {}): Promise<{ envelope?: ArchiveEnvelope | null }> {
|
|
// The archive lives on this app's server; the static Lite build has none.
|
|
if (IS_LITE) throw new Error('disabled');
|
|
const res = await apiFetch('/api/account-archive', {
|
|
cache: 'no-store', ...init,
|
|
headers: {
|
|
'x-archive-username': owner.username, 'x-archive-server': owner.serverUrl,
|
|
...(init.body ? { 'Content-Type': 'application/json' } : {}),
|
|
},
|
|
});
|
|
const body = await res.json().catch(() => ({}));
|
|
if (!res.ok) throw new Error(typeof body.error === 'string' ? body.error : 'storage_failed');
|
|
return body;
|
|
}
|
|
|
|
export async function fetchArchive(owner: ArchiveOwner): Promise<ArchiveEnvelope | null> {
|
|
return (await request(owner)).envelope ?? null;
|
|
}
|
|
export async function putArchive(owner: ArchiveOwner, envelope: ArchiveEnvelope): Promise<void> {
|
|
await request(owner, { method: 'PUT', body: JSON.stringify({ envelope }) });
|
|
}
|
|
export async function removeArchive(owner: ArchiveOwner): Promise<void> {
|
|
await request(owner, { method: 'DELETE' });
|
|
}
|
|
|
|
/**
|
|
* This browser's accounts as the archive keeps them. With `includePasswords`,
|
|
* an account signed in here with a plain password carries it; tokens and
|
|
* accounts not signed in carry nothing.
|
|
*/
|
|
export function collectAccounts(includePasswords = false): ArchivedAccount[] {
|
|
const auth = useAuthStore.getState();
|
|
return useAccountStore.getState().accounts.map(a => {
|
|
const entry: ArchivedAccount = { username: a.username, serverUrl: a.serverUrl, label: a.label, avatarColor: a.avatarColor };
|
|
const password = includePasswords && a.authMode === 'basic' ? auth.getClientForAccount(a.id)?.getBasicPassword() : null;
|
|
if (password) entry.password = password;
|
|
return entry;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Adds the archived accounts this browser lacks, each waiting for its own
|
|
* sign-in, and gives the ones it has their archived label and colour.
|
|
*/
|
|
export function restoreAccounts(archived: ArchivedAccount[]): { added: number; updated: number; skipped: number } {
|
|
const store = useAccountStore.getState();
|
|
let added = 0, updated = 0, skipped = 0;
|
|
for (const entry of archived) {
|
|
const id = generateAccountId(entry.username, entry.serverUrl);
|
|
if (store.getAccountById(id)) {
|
|
store.updateAccount(id, { label: entry.label, avatarColor: entry.avatarColor });
|
|
updated++;
|
|
continue;
|
|
}
|
|
if (useAccountStore.getState().accounts.length >= getMaxAccounts()) { skipped++; continue; }
|
|
store.addAccount({
|
|
username: entry.username, serverUrl: entry.serverUrl, authMode: 'basic', rememberMe: false,
|
|
label: entry.label, displayName: entry.label, email: entry.username, lastLoginAt: 0,
|
|
isConnected: false, hasError: true, isDefault: false, awaitingSignIn: true,
|
|
});
|
|
store.updateAccount(id, { avatarColor: entry.avatarColor });
|
|
added++;
|
|
}
|
|
return { added, updated, skipped };
|
|
}
|
|
|
|
/**
|
|
* Restores the list, then signs in one by one to the accounts saved with a
|
|
* password that are not signed in here yet. An account whose password no
|
|
* longer works keeps waiting for a sign-in by hand.
|
|
*
|
|
* A restored sign-in is remembered only when the server allows it and the
|
|
* account doing the restore was itself signed in with "Remember me": on a
|
|
* shared computer, restoring must not leave long-lived sessions behind.
|
|
*/
|
|
export async function restoreArchive(archived: ArchivedAccount[], rememberMeAllowed: boolean) {
|
|
const accounts = useAccountStore.getState();
|
|
const active = accounts.accounts.find(a => a.id === accounts.activeAccountId);
|
|
const rememberMe = rememberMeAllowed && !!active?.rememberMe;
|
|
const result = restoreAccounts(archived);
|
|
let signedIn = 0, signInFailed = 0;
|
|
for (const entry of archived) {
|
|
if (!entry.password) continue;
|
|
const id = generateAccountId(entry.username, entry.serverUrl);
|
|
const account = useAccountStore.getState().getAccountById(id);
|
|
if (!account || account.authMode !== 'basic' || useAuthStore.getState().getClientForAccount(id)) continue;
|
|
if (await useAuthStore.getState().signInRestoredAccount(id, entry.password, rememberMe)) signedIn++;
|
|
else signInFailed++;
|
|
}
|
|
const waiting = archived.filter(e => useAccountStore.getState()
|
|
.getAccountById(generateAccountId(e.username, e.serverUrl))?.awaitingSignIn).length;
|
|
return { ...result, signedIn, signInFailed, waiting };
|
|
}
|