merge: integrate secure remembered password
This commit is contained in:
@@ -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" />}
|
||||
登录
|
||||
|
||||
@@ -67,6 +67,7 @@ type RefreshSessionOptions = {
|
||||
type PasswordLoginInput = {
|
||||
username: string;
|
||||
password: string;
|
||||
rememberPassword?: boolean;
|
||||
};
|
||||
|
||||
type MobileLoginInput = {
|
||||
|
||||
Reference in New Issue
Block a user