feat(auth): remember encrypted local login credentials

This commit is contained in:
justme committed 2026-10-13 01:39:24 +08:00
1 parent f6cfb12716
commit b65e32885f
9 files changed
+148 -30

No files matched your search

@@ -77,6 +77,18 @@
grid-column: 1 / -1;
}
.form .remember {
grid-column: 1 / -1;
flex-direction: row;
align-items: center;
gap: var(--space-2);
}
.form .remember input {
padding: 0;
accent-color: var(--accent);
}
.error {
color: var(--danger);
}
+32 -5
View File
@@ -1,4 +1,5 @@
import { useState, type FormEvent } from 'react';
import { useEffect, useRef, useState, type FormEvent } from 'react';
import { accountApi } from '@/core-bridge/account';
import { useAccount } from '@/store/account';
import { useChannels } from '@/store/channels';
import { useShares } from '@/store/shares';
@@ -16,6 +17,8 @@ export function AccountPanel() {
const login = useAccount((s) => s.login);
const logout = useAccount((s) => s.logout);
const sync = useAccount((s) => s.sync);
const remembered = useAccount((s) => s.remembered);
const clearSavedLogin = useAccount((s) => s.clearSavedLogin);
const channelsReady = useChannels((s) => s.hydrated);
const sharesReady = useShares((s) => s.hydrated);
const refreshChannels = useChannels((s) => s.refresh);
@@ -24,12 +27,31 @@ export function AccountPanel() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [message, setMessage] = useState('');
const [remember, setRemember] = useState(true);
const [saved, setSaved] = useState(false);
const edited = useRef(false);
useEffect(() => {
if (account) return;
let active = true;
void accountApi.savedLogin().then((fields) => {
if (!active || edited.current) return;
setSaved(fields.saved);
if (fields.saved) { setUsername(fields.username); setPassword(fields.password); setRegister(false); setRemember(true); }
}).catch(() => { if (active) setMessage('本机保存读取失败,可手动输入登录。'); });
return () => { active = false; };
}, [account]);
async function clearSaved() {
edited.current = true;
try { await clearSavedLogin(); setSaved(false); setRemember(false); setUsername(''); setPassword(''); setMessage(account ? '本机保存已清除,当前页面仍可使用;刷新后需要重新登录。' : '已清除本机保存的账号和密码。'); }
catch { /* 错误由账号状态呈现 */ }
}
async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (busy) return;
setMessage('');
try {
await login(username, password, register);
await login(username, password, register, remember);
setPassword('');
await Promise.all([refreshChannels(), refreshShares()]);
setMessage(register ? '账号已创建,资源保险库已同步。' : '已登录,资源保险库已同步。');
@@ -52,14 +74,19 @@ export function AccountPanel() {
<div className={styles.actions}>
<Button disabled={busy} onClick={() => { void sync().then(() => setMessage('资源保险库已同步。')).catch(() => {}); }}>{busy ? '同步中…' : '立即同步'}</Button>
<Button variant="ghost" disabled={busy} onClick={() => { void logout().then(() => navigate('/')); setMessage(''); }}>退出登录</Button>
{remembered ? <Button variant="ghost" disabled={busy} onClick={() => { void clearSaved(); }}>清除本机保存</Button> : null}
<p className={styles.note}>{remembered ? '已开启本机记住登录。' : '仅本次登录,刷新或关闭页面后需重新登录。'}</p>
</div>
) : (
<form className={styles.form} onSubmit={(event) => { void submit(event); }}>
<label>用户名<input autoComplete="username" minLength={3} maxLength={32} value={username} onChange={(event) => setUsername(event.target.value)} required /></label>
<label>密码<input autoComplete={register ? 'new-password' : 'current-password'} type="password" minLength={register ? 12 : undefined} value={password} onChange={(event) => setPassword(event.target.value)} required /></label>
<label>用户名<input name="username" autoComplete="username" minLength={3} maxLength={32} value={username} onChange={(event) => { edited.current = true; setUsername(event.target.value); }} required /></label>
<label>密码<input name="password" autoComplete={register ? 'new-password' : 'current-password'} type="password" minLength={register ? 12 : undefined} value={password} onChange={(event) => { edited.current = true; setPassword(event.target.value); }} required /></label>
<label className={styles.remember}><input type="checkbox" checked={remember} onChange={(event) => setRemember(event.target.checked)} />记住账号和密码(本机)</label>
<p className={styles.note}>仅在自己的设备使用。退出登录后仍可自动填充;“清除本机保存”会删除已保存资料。</p>
<div className={styles.actions}>
<Button type="submit" variant="primary" disabled={busy || !channelsReady || !sharesReady}>{busy ? '处理中…' : !channelsReady || !sharesReady ? '正在读取本机资源…' : register ? '创建账号' : '登录并同步'}</Button>
<Button variant="ghost" onClick={() => { setRegister((value) => !value); setMessage(''); }}>{register ? '已有账号' : '创建账号'}</Button>
<Button variant="ghost" onClick={() => { edited.current = true; setRegister((value) => !value); setMessage(''); }}>{register ? '已有账号' : '创建账号'}</Button>
{saved ? <Button variant="ghost" disabled={busy} onClick={() => { void clearSaved(); }}>清除本机保存</Button> : null}
</div>
</form>
)}