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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user