feat(auth): protect aggregate console with password login

This commit is contained in:
chick
2026-07-19 17:13:43 +08:00
parent 75682b8134
commit f2803896a8
16 changed files with 1165 additions and 11 deletions
+138
View File
@@ -0,0 +1,138 @@
import { type FormEvent, useEffect, useState } from 'react';
import type { ConsoleAuthDataSource, ConsoleAuthStatus } from './console-auth.js';
import { createConsoleAuthApiDataSource } from './console-auth.js';
export function ConsoleAuthSettings({
dataSource,
}: {
readonly dataSource?: ConsoleAuthDataSource;
}) {
const [source] = useState(() => dataSource ?? createConsoleAuthApiDataSource());
const [status, setStatus] = useState<ConsoleAuthStatus>();
const [enabled, setEnabled] = useState(false);
const [password, setPassword] = useState('');
const [confirmation, setConfirmation] = useState('');
const [saving, setSaving] = useState(false);
const [notice, setNotice] = useState('');
const [error, setError] = useState('');
useEffect(() => {
let active = true;
void source.status().then(
(next) => {
if (!active) return;
setStatus(next);
setEnabled(next.protectionEnabled);
},
() => {
if (active) setError('无法读取密码保护设置。');
},
);
return () => {
active = false;
};
}, [source]);
const save = async (event: FormEvent) => {
event.preventDefault();
if (!status || saving) return;
setNotice('');
setError('');
if (enabled && !status.configured) {
if (password.length < 8 || !/[A-Za-z]/u.test(password) || !/\d/u.test(password)) {
setError('密码至少 8 位,并同时包含字母和数字。');
return;
}
if (password !== confirmation) {
setError('两次输入的密码不一致。');
return;
}
}
setSaving(true);
try {
const input =
enabled && !status.configured ? { enabled: true, newPassword: password } : { enabled };
const next = await source.update(input);
setStatus(next);
setEnabled(next.protectionEnabled);
setPassword('');
setConfirmation('');
setNotice(next.protectionEnabled ? '密码保护已启用。' : '密码保护已关闭。');
} catch {
setError('保存失败,请确认当前登录状态后重试。');
} finally {
setSaving(false);
}
};
return (
<section className="settings-page" aria-labelledby="password-protection-title">
<header className="page-heading">
<p className="eyebrow">SYSTEM SETTINGS</p>
<h1 id="password-protection-title"></h1>
<p>访</p>
<p> HTTP使 HTTPS</p>
<p> SimAdmin 访</p>
</header>
{error && !status ? <p role="alert">{error}</p> : null}
{status ? (
<form className="settings-card auth-settings" onSubmit={(event) => void save(event)}>
<label className="toggle-row">
<span>
<strong></strong>
<small>访</small>
</span>
<input
type="checkbox"
checked={enabled}
onChange={(event) => {
setEnabled(event.target.checked);
setNotice('');
}}
/>
</label>
{enabled && !status.configured ? (
<div className="auth-password-fields">
<label htmlFor="new-console-password">访</label>
<input
id="new-console-password"
type="password"
autoComplete="new-password"
value={password}
onChange={(event) => setPassword(event.target.value)}
/>
<label htmlFor="confirm-console-password">访</label>
<input
id="confirm-console-password"
type="password"
autoComplete="new-password"
value={confirmation}
onChange={(event) => setConfirmation(event.target.value)}
/>
<small> 8 </small>
</div>
) : null}
{status.configured ? <p>访 API </p> : null}
{error ? <p role="alert">{error}</p> : null}
{notice ? <p role="status">{notice}</p> : null}
<button type="submit" disabled={saving}>
{saving ? '正在保存…' : '保存密码保护设置'}
</button>
{status.protectionEnabled && status.authenticated ? (
<button
type="button"
onClick={() => {
void source.logout().then(() => window.location.reload());
}}
>
退
</button>
) : null}
</form>
) : !error ? (
<p role="status"></p>
) : null}
</section>
);
}