feat(auth): remember encrypted local login credentials
This commit is contained in:
1 parent
f6cfb12716
commit
b65e32885f
9 files changed
+148
-30
No files matched your search
@@ -7,11 +7,12 @@ import { mergeVault, validateVault, validatePersonal, type VaultProfile, type Pe
|
||||
import { joinChannel } from './channelResources';
|
||||
import { openChannelGrant, sealChannelGrant } from './channelGrant';
|
||||
import { recoverAccountChannels } from './accountChannels';
|
||||
import { forgetAccount, localAccountDevice, rememberedAccount, rememberAccount, saveDeviceName } from './accountPersistence';
|
||||
import { forgetAccount, localAccountDevice, rememberedAccount, rememberAccount, saveDeviceName, saveLoginFields, forgetLoginFields, savedLoginFields } from './accountPersistence';
|
||||
|
||||
interface VaultBody { revision: number; vault: string }
|
||||
interface AccountSession {
|
||||
username: string; token: string; key: CryptoKey; profile: VaultProfile; revision: number;
|
||||
remember: boolean;
|
||||
controller: AbortController; dirty: boolean; syncing?: Promise<void>; error?: string;
|
||||
timer?: ReturnType<typeof setTimeout>; polling?: ReturnType<typeof setInterval>;
|
||||
}
|
||||
@@ -31,6 +32,10 @@ async function applyProfile(profile: VaultProfile): Promise<void> {
|
||||
await importResources(profile.resources.map((row) => ({ ...row, endpointSecret: new Uint8Array(32) } as SavedResource)));
|
||||
await importResourceDeletes(profile.deletes);
|
||||
}
|
||||
async function persistSession(session: AccountSession, value: VaultBody): Promise<void> {
|
||||
if (session.remember) await rememberAccount({ username: session.username, token: session.token, key: session.key, ...value });
|
||||
else await forgetAccount();
|
||||
}
|
||||
async function syncVault(): Promise<void> {
|
||||
if (!account) throw new Error('请先登录账号');
|
||||
const session = account;
|
||||
@@ -48,7 +53,7 @@ async function syncVault(): Promise<void> {
|
||||
stillActive(session); await applyProfile(merged);
|
||||
session.profile = merged; session.revision = remote.revision; session.error = undefined;
|
||||
setAccountIdentity(session, merged.keys.publicKey, merged.personal);
|
||||
await rememberAccount({ username: session.username, token: session.token, key: session.key, ...remote });
|
||||
await persistSession(session, remote);
|
||||
break;
|
||||
}
|
||||
const vault = await sealJson(merged, session.key);
|
||||
@@ -60,7 +65,7 @@ async function syncVault(): Promise<void> {
|
||||
await applyProfile(merged);
|
||||
session.profile = merged; session.revision = result.revision; session.error = undefined;
|
||||
setAccountIdentity(session, merged.keys.publicKey, merged.personal);
|
||||
await rememberAccount({ username: session.username, token: session.token, key: session.key, vault, revision: result.revision });
|
||||
await persistSession(session, { vault, revision: result.revision });
|
||||
break;
|
||||
} catch (error) {
|
||||
if (attempt === 2 || !(error instanceof Error && 'status' in error && error.status === 409)) throw error;
|
||||
@@ -73,16 +78,17 @@ async function syncVault(): Promise<void> {
|
||||
account = undefined; clearTimeout(session.timer); clearInterval(session.polling); session.controller.abort();
|
||||
setResourceChangeListener(undefined); setAccountIdentity(undefined); clearResourceSessions(); setResourceScope(null);
|
||||
void forgetAccount();
|
||||
void forgetLoginFields();
|
||||
}
|
||||
throw error;
|
||||
}).finally(() => { session.syncing = undefined; });
|
||||
return session.syncing;
|
||||
}
|
||||
|
||||
async function activate(identity: { username: string; token: string }, key: CryptoKey, remote: VaultBody): Promise<AccountSession> {
|
||||
async function activate(identity: { username: string; token: string }, key: CryptoKey, remote: VaultBody, remember = true): Promise<AccountSession> {
|
||||
const profile = validateVault(await openJson(remote.vault, key));
|
||||
clearResourceSessions(); setResourceScope(identity.username);
|
||||
const session: AccountSession = { ...identity, key, profile, revision: remote.revision, controller: new AbortController(), dirty: false };
|
||||
const session: AccountSession = { ...identity, key, profile, revision: remote.revision, remember, controller: new AbortController(), dirty: false };
|
||||
account = session; setAccountIdentity(identity, profile.keys.publicKey, profile.personal);
|
||||
await applyProfile(mergeVault(profile, await listAllResources(), await listResourceDeletes()));
|
||||
setResourceChangeListener(scheduleSync);
|
||||
@@ -98,7 +104,7 @@ export function restoreAccount(): Promise<void> {
|
||||
try {
|
||||
remote = await apiFetch('/accounts/vault', { headers: headers(saved) }).then((r) => r.json() as Promise<VaultBody>);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && 'status' in error && error.status === 401) { await forgetAccount(); return; }
|
||||
if (error instanceof Error && 'status' in error && error.status === 401) { await forgetAccount(); await forgetLoginFields(); return; }
|
||||
remote = saved;
|
||||
}
|
||||
try {
|
||||
@@ -117,7 +123,7 @@ function scheduleSync(): void {
|
||||
account.timer = setTimeout(() => { void syncVault().catch(() => {}); }, 700);
|
||||
}
|
||||
|
||||
export async function registerAccount(input: string, password: string): Promise<Awaited<ReturnType<typeof accountStatus>>> {
|
||||
export async function registerAccount(input: string, password: string, remember = true): Promise<Awaited<ReturnType<typeof accountStatus>>> {
|
||||
if (account) throw new Error('请先退出当前账号');
|
||||
const username = usernameOf(input);
|
||||
if (password.length < 12 || password.length > 1024) throw new Error('密码长度需为 12–1024 个字符');
|
||||
@@ -128,9 +134,9 @@ export async function registerAccount(input: string, password: string): Promise<
|
||||
await apiFetch('/accounts', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, salt: b64(salt), verifier: b64(verifier), public_key: JSON.stringify(profile.keys.publicKey), vault: await sealJson(profile, key) }) });
|
||||
} finally { verifier.fill(0); }
|
||||
return loginAccount(username, password);
|
||||
return loginAccount(username, password, remember);
|
||||
}
|
||||
export async function loginAccount(input: string, password: string): Promise<Awaited<ReturnType<typeof accountStatus>>> {
|
||||
export async function loginAccount(input: string, password: string, remember = true): Promise<Awaited<ReturnType<typeof accountStatus>>> {
|
||||
if (account) throw new Error('请先退出当前账号');
|
||||
const username = usernameOf(input);
|
||||
const challenge = await apiFetch('/accounts/salt', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username }) })
|
||||
@@ -148,8 +154,9 @@ export async function loginAccount(input: string, password: string): Promise<Awa
|
||||
const publicKey = await apiFetch(`/accounts/users/${username}/key`, { headers: headers(identity) }).then((r) => r.json() as Promise<{ public_key: string }>);
|
||||
const published = JSON.parse(publicKey.public_key) as JsonWebKey;
|
||||
if (published.x !== profile.keys.publicKey.x || published.y !== profile.keys.publicKey.y) throw new Error('账号邀请公钥与保险库不匹配');
|
||||
await activate(identity, key, remote);
|
||||
await rememberAccount({ ...identity, key, ...remote });
|
||||
const session = await activate(identity, key, remote, remember);
|
||||
if (remember) await saveLoginFields(username, password); else await forgetLoginFields();
|
||||
await persistSession(session, remote);
|
||||
await syncVault().catch(() => {});
|
||||
return accountStatus();
|
||||
} catch (error) {
|
||||
@@ -158,8 +165,9 @@ export async function loginAccount(input: string, password: string): Promise<Awa
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
export async function accountStatus(): Promise<{ username: string | null; revision: number; personal?: PersonalProfile; error?: string }> {
|
||||
export async function accountStatus(): Promise<{ username: string | null; revision: number; remembered: boolean; personal?: PersonalProfile; error?: string }> {
|
||||
return { username: account?.username ?? null, revision: account?.revision ?? 0,
|
||||
remembered: account?.remember ?? false,
|
||||
...(account?.profile.personal ? { personal: account.profile.personal } : {}),
|
||||
...(account?.error ? { error: '账号资源同步失败,请重试。' } : {}) };
|
||||
}
|
||||
@@ -171,12 +179,23 @@ export async function logoutAccount(): Promise<void> {
|
||||
if (session) void apiFetch('/accounts/session', { method: 'DELETE', headers: headers(session) }).catch(() => {});
|
||||
}
|
||||
export async function syncAccount(): Promise<void> { await syncVault(); }
|
||||
export async function getSavedLogin(): Promise<{ username: string; password: string; saved: boolean }> {
|
||||
if (account) return { username: '', password: '', saved: false };
|
||||
const fields = await savedLoginFields(); return fields ? { ...fields, saved: true } : { username: '', password: '', saved: false };
|
||||
}
|
||||
export async function clearSavedLogin(): Promise<void> {
|
||||
if (account) {
|
||||
account.remember = false;
|
||||
await account.syncing?.catch(() => {});
|
||||
}
|
||||
await forgetAccount(); await forgetLoginFields();
|
||||
}
|
||||
export async function updatePersonal(displayName: string, avatar?: string): Promise<void> {
|
||||
if (!account) throw new Error('请先登录账号');
|
||||
account.profile = { ...account.profile, personal: validatePersonal({ displayName, ...(avatar ? { avatar } : {}), modifiedAt: Date.now() }) };
|
||||
setAccountIdentity(account, account.profile.keys.publicKey, account.profile.personal);
|
||||
// Retain an offline profile edit for the next session restoration.
|
||||
await rememberAccount({ username: account.username, token: account.token, key: account.key, vault: await sealJson(account.profile, account.key), revision: account.revision });
|
||||
await persistSession(account, { vault: await sealJson(account.profile, account.key), revision: account.revision });
|
||||
await syncVault();
|
||||
}
|
||||
export async function listAccountDevices(): Promise<unknown> { requireAccount(); return apiFetch('/accounts/devices').then((r) => r.json()); }
|
||||
|
||||
Reference in new issue
Block a user