mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-05 13:27:55 +08:00
✨ feat(web-auth): 支持六位密码与环境变量托管
- 密码策略统一为至少六个可见字符,覆盖初始化与设置中心改密 - 支持 GONAVI_WEB_PASSWORD 启动同步并保留既有 2FA 与会话配置 - 设置中心识别环境托管状态并禁用临时改密 - Compose、env 示例及双语文档补充密码配置和容器重建说明 - 增加认证持久化、环境覆盖、前端状态与 Docker 登录回归校验
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./WebAuthSettingsPanel.tsx', import.meta.url), 'utf8');
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'];
|
||||
|
||||
describe('WebAuthSettingsPanel environment-managed password', () => {
|
||||
it('disables password changes and explains how to update the password', () => {
|
||||
expect(source).toContain('passwordManagedByEnvironment: boolean;');
|
||||
expect(source).toContain('summary?.passwordManagedByEnvironment === true');
|
||||
expect(source).toContain("t('app.settings.web_auth.password.managed_by_environment')");
|
||||
expect(source).toContain('disabled={passwordManagedByEnvironment}');
|
||||
});
|
||||
|
||||
it('keeps the six-character and environment-managed copy in every locale', () => {
|
||||
for (const locale of locales) {
|
||||
const catalog = JSON.parse(
|
||||
readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8'),
|
||||
) as Record<string, string>;
|
||||
expect(catalog['app.settings.web_auth.password.new_placeholder']).toContain('6');
|
||||
expect(catalog['app.settings.web_auth.password.managed_by_environment']).toContain('GONAVI_WEB_PASSWORD');
|
||||
expect(catalog['web_auth.error.password_managed_by_environment']).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,7 @@ type WebAuthSettingsSummary = {
|
||||
sessionAbsoluteHours: number;
|
||||
sessionRememberDays: number;
|
||||
updatedAt?: string;
|
||||
passwordManagedByEnvironment: boolean;
|
||||
};
|
||||
|
||||
type WebAuthSettingsPanelProps = {
|
||||
@@ -80,6 +81,7 @@ const WebAuthSettingsPanel: React.FC<WebAuthSettingsPanelProps> = ({
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [passwordForm, setPasswordForm] = useState<PasswordFormState>(() => createEmptyPasswordFormState());
|
||||
const passwordManagedByEnvironment = summary?.passwordManagedByEnvironment === true;
|
||||
|
||||
const summaryItems = useMemo(() => {
|
||||
if (!summary) {
|
||||
@@ -245,9 +247,11 @@ const WebAuthSettingsPanel: React.FC<WebAuthSettingsPanelProps> = ({
|
||||
<div style={{ display: 'grid', gap: 4 }}>
|
||||
<div style={sectionTitleStyle(titleColor)}>{t('app.settings.web_auth.password.title')}</div>
|
||||
<div style={{ color: mutedColor, fontSize: 13, lineHeight: 1.6 }}>
|
||||
{summary?.totpEnabled
|
||||
? t('app.settings.web_auth.password.description_with_code')
|
||||
: t('app.settings.web_auth.password.description')}
|
||||
{passwordManagedByEnvironment
|
||||
? t('app.settings.web_auth.password.managed_by_environment')
|
||||
: summary?.totpEnabled
|
||||
? t('app.settings.web_auth.password.description_with_code')
|
||||
: t('app.settings.web_auth.password.description')}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -261,6 +265,7 @@ const WebAuthSettingsPanel: React.FC<WebAuthSettingsPanelProps> = ({
|
||||
<span style={{ color: mutedColor, fontSize: 12 }}>{t('app.settings.web_auth.password.current_label')}</span>
|
||||
<Input.Password
|
||||
autoComplete="current-password"
|
||||
disabled={passwordManagedByEnvironment}
|
||||
value={passwordForm.currentPassword}
|
||||
placeholder={t('app.settings.web_auth.password.current_placeholder')}
|
||||
onChange={(event) => updatePasswordField('currentPassword', event.target.value)}
|
||||
@@ -271,6 +276,7 @@ const WebAuthSettingsPanel: React.FC<WebAuthSettingsPanelProps> = ({
|
||||
<span style={{ color: mutedColor, fontSize: 12 }}>{t('app.settings.web_auth.password.code_label')}</span>
|
||||
<Input
|
||||
autoComplete="one-time-code"
|
||||
disabled={passwordManagedByEnvironment}
|
||||
inputMode="numeric"
|
||||
value={passwordForm.code}
|
||||
placeholder={t('app.settings.web_auth.password.code_placeholder')}
|
||||
@@ -282,6 +288,7 @@ const WebAuthSettingsPanel: React.FC<WebAuthSettingsPanelProps> = ({
|
||||
<span style={{ color: mutedColor, fontSize: 12 }}>{t('app.settings.web_auth.password.new_label')}</span>
|
||||
<Input.Password
|
||||
autoComplete="new-password"
|
||||
disabled={passwordManagedByEnvironment}
|
||||
value={passwordForm.newPassword}
|
||||
placeholder={t('app.settings.web_auth.password.new_placeholder')}
|
||||
onChange={(event) => updatePasswordField('newPassword', event.target.value)}
|
||||
@@ -291,6 +298,7 @@ const WebAuthSettingsPanel: React.FC<WebAuthSettingsPanelProps> = ({
|
||||
<span style={{ color: mutedColor, fontSize: 12 }}>{t('app.settings.web_auth.password.confirm_label')}</span>
|
||||
<Input.Password
|
||||
autoComplete="new-password"
|
||||
disabled={passwordManagedByEnvironment}
|
||||
value={passwordForm.confirmPassword}
|
||||
placeholder={t('app.settings.web_auth.password.confirm_placeholder')}
|
||||
onChange={(event) => updatePasswordField('confirmPassword', event.target.value)}
|
||||
@@ -298,7 +306,12 @@ const WebAuthSettingsPanel: React.FC<WebAuthSettingsPanelProps> = ({
|
||||
</label>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-start' }}>
|
||||
<Button type="primary" loading={submitting} onClick={() => { void handleSubmit(); }}>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={submitting}
|
||||
disabled={passwordManagedByEnvironment}
|
||||
onClick={() => { void handleSubmit(); }}
|
||||
>
|
||||
{t('app.settings.web_auth.password.submit')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user