Improve local auth env handling and fallbacks

Allow local env files to reliably override authentication feature flags (SMS/WeChat) by whitelisting keys in scripts/dev-utils.mjs and adding a unit test. Add SMS checks to scripts/check-api-server-env.mjs. Make server config.parse_bool tolerant of shell-wrapped quoted values (e.g. '"true"') and add tests so SMS_AUTH_ENABLED is parsed correctly when shells supply quotes. Update docs to clarify SMS env behaviour, restart requirements, and add guidance + a CSS fallback for old mobile browsers (QQ/X5) so public cover images render even when aspect-ratio is unsupported. Also include related frontend test and component adjustments and add puzzle onboarding handlers/endpoints in server-rs/crates/api-server/src/puzzle.rs.
This commit is contained in:
2026-05-18 23:13:49 +08:00
parent 4c10c181e3
commit d1adfa3406
22 changed files with 4309 additions and 52 deletions

View File

@@ -316,7 +316,7 @@ test('auth gate does not auto-create a guest account when dev guest switch is no
expect(await screen.findByText('应用内容')).toBeTruthy();
});
test('auth gate keeps password entry available when login options are empty', async () => {
test('auth gate keeps sms and password entries available when login options are empty', async () => {
const user = userEvent.setup();
authMocks.getCurrentAuthUser.mockResolvedValue({
@@ -336,12 +336,19 @@ test('auth gate keeps password entry available when login options are empty', as
await user.click(await screen.findByRole('button', { name: '进入作品' }));
const dialog = screen.getByRole('dialog', { name: '账号入口' });
expect(within(dialog).getByRole('tab', { name: '短信登录' })).toBeTruthy();
expect(within(dialog).getByRole('tab', { name: '密码登录' })).toBeTruthy();
expect(within(dialog).getByLabelText('验证码')).toBeTruthy();
expect(
within(dialog).getByRole('button', { name: '获取验证码' }),
).toBeTruthy();
await user.click(within(dialog).getByRole('tab', { name: '密码登录' }));
expect(within(dialog).getByLabelText('密码')).toBeTruthy();
expect(within(dialog).queryByText('当前登录入口暂不可用。')).toBeNull();
expect(within(dialog).queryByText('读取登录方式失败')).toBeNull();
});
test('auth gate falls back to password entry when login options request fails', async () => {
test('auth gate keeps sms and password entries available when login options request fails', async () => {
const user = userEvent.setup();
authMocks.getAuthLoginOptions.mockRejectedValue(
@@ -357,6 +364,13 @@ test('auth gate falls back to password entry when login options request fails',
await user.click(await screen.findByRole('button', { name: '进入作品' }));
const dialog = screen.getByRole('dialog', { name: '账号入口' });
expect(within(dialog).getByRole('tab', { name: '短信登录' })).toBeTruthy();
expect(within(dialog).getByRole('tab', { name: '密码登录' })).toBeTruthy();
expect(within(dialog).getByLabelText('验证码')).toBeTruthy();
expect(
within(dialog).getByRole('button', { name: '获取验证码' }),
).toBeTruthy();
await user.click(within(dialog).getByRole('tab', { name: '密码登录' }));
expect(within(dialog).getByLabelText('密码')).toBeTruthy();
expect(within(dialog).queryByText('当前登录入口暂不可用。')).toBeNull();
});

View File

@@ -61,7 +61,7 @@ type AuthStatus =
| 'ready'
| 'error';
const FALLBACK_LOGIN_METHODS: AuthLoginMethod[] = ['password'];
const REQUIRED_LOGIN_METHODS: AuthLoginMethod[] = ['phone', 'password'];
function readInviteCodeFromLocation(): string {
const params = new URLSearchParams(window.location.search || '');
@@ -76,11 +76,13 @@ function normalizeAvailableLoginMethods(
): AuthLoginMethod[] {
const normalizedMethods = Array.from(new Set(methods ?? []));
// 密码登录由 Rust auth entry 固定承载,不依赖短信或微信环境开关
// 当 login-options 联调失败或配置返回空数组时,仍要保留账号入口,避免登录弹窗失去可操作方式。
return normalizedMethods.length > 0
? normalizedMethods
: FALLBACK_LOGIN_METHODS;
// 登录面板的核心入口必须稳定展示login-options 只补充微信环境相关入口
return Array.from(
new Set<AuthLoginMethod>([
...REQUIRED_LOGIN_METHODS,
...normalizedMethods,
]),
);
}
type AuthHydrateSessionResult =
@@ -367,9 +369,9 @@ export function AuthGate({ children }: AuthGateProps) {
return;
}
setAvailableLoginMethods(FALLBACK_LOGIN_METHODS);
setAvailableLoginMethods(REQUIRED_LOGIN_METHODS);
setUser(null);
// 中文注释:登录方式接口失败时按产品约定保留密码登录入口;
// 中文注释:登录方式接口失败时按产品约定保留验证码和密码登录入口;
// 这里不展示接口读取错误,避免用户误以为登录本身不可用。
setError(callbackResult?.error ?? '');
setStatus('unauthenticated');

View File

@@ -80,8 +80,8 @@ export function LoginScreen({
const [legalConsentChecked, setLegalConsentChecked] = useState(false);
const [activeLegalDocumentId, setActiveLegalDocumentId] =
useState<LegalDocumentId | null>(null);
const passwordLoginEnabled = availableLoginMethods.includes('password');
const phoneLoginEnabled = availableLoginMethods.includes('phone');
const passwordLoginEnabled = true;
const phoneLoginEnabled = true;
const wechatLoginEnabled = availableLoginMethods.includes('wechat');
const [activeLoginTab, setActiveLoginTab] = useState<LoginTab>('phone');