merge: integrate secure remembered password

This commit is contained in:
2026-08-20 23:41:48 +08:00
11 changed files with 530 additions and 17 deletions

View File

@@ -42,6 +42,15 @@ type SmsResponse = {
error?: unknown;
};
type RememberedPasswordResponse = {
success?: unknown;
available?: unknown;
credentials?: {
username?: unknown;
password?: unknown;
} | null;
};
const EMPTY_LINKS: PublicLinks = {
termsUrl: null,
privacyUrl: null,
@@ -104,6 +113,8 @@ export function Login() {
const [mode, setMode] = useState<LoginMode>('password');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [rememberPassword, setRememberPassword] = useState(false);
const [rememberPasswordAvailable, setRememberPasswordAvailable] = useState(false);
const [phone, setPhone] = useState('');
const [imageCode, setImageCode] = useState('');
const [smsCode, setSmsCode] = useState('');
@@ -123,13 +134,41 @@ export function Login() {
useEffect(() => {
let active = true;
void hostApiFetch<PublicConfigResponse>('/api/auth/public-config', { cache: 'no-store' })
.then((response) => {
void (async () => {
try {
const response = await hostApiFetch<PublicConfigResponse>(
'/api/auth/public-config',
{ cache: 'no-store' },
);
if (active) setLinks(projectPublicLinks(response));
})
.catch(() => {
} catch {
if (active) setLinks(EMPTY_LINKS);
});
}
try {
const response = await hostApiFetch<RememberedPasswordResponse>(
'/api/auth/remembered-password',
{ cache: 'no-store' },
);
if (!active) return;
const available = response.success === true && response.available === true;
setRememberPasswordAvailable(available);
if (
available
&& response.credentials
&& typeof response.credentials.username === 'string'
&& response.credentials.username.trim()
&& typeof response.credentials.password === 'string'
&& response.credentials.password
) {
setUsername(response.credentials.username.trim());
setPassword(response.credentials.password);
setRememberPassword(true);
}
} catch {
if (active) setRememberPasswordAvailable(false);
}
})();
return () => {
active = false;
};
@@ -229,7 +268,11 @@ export function Login() {
const handlePasswordSubmit = (event: FormEvent) => {
event.preventDefault();
if (!agreed || !username.trim() || !password) return;
void finishLogin(() => loginWithPassword({ username: username.trim(), password }));
void finishLogin(() => loginWithPassword({
username: username.trim(),
password,
rememberPassword,
}));
};
const handleMobileSubmit = (event: FormEvent) => {
@@ -349,6 +392,18 @@ export function Login() {
</div>
<Input id="login-password" type="password" autoComplete="current-password" value={password} onChange={(event) => setPassword(event.target.value)} />
</div>
<label
className="flex w-fit items-center gap-2 text-sm text-muted-foreground"
title={rememberPasswordAvailable ? undefined : '当前环境无法使用系统安全存储'}
>
<input
type="checkbox"
checked={rememberPassword}
disabled={!rememberPasswordAvailable}
onChange={(event) => setRememberPassword(event.target.checked)}
/>
<span></span>
</label>
<Button type="submit" className="w-full" disabled={!passwordReady}>
{busy && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}

View File

@@ -67,6 +67,7 @@ type RefreshSessionOptions = {
type PasswordLoginInput = {
username: string;
password: string;
rememberPassword?: boolean;
};
type MobileLoginInput = {