feat(auth): remember password securely
This commit is contained in:
1 parent
1c6b004436
commit
990639f770
11 files changed
+530
-17
No files matched your search
@@ -86,6 +86,9 @@ describe('Login page', () => {
|
||||
},
|
||||
};
|
||||
}
|
||||
if (path === '/api/auth/remembered-password') {
|
||||
return { success: true, available: true, credentials: null };
|
||||
}
|
||||
if (path.startsWith('/api/auth/mobile-image-code?')) return resolveCaptcha();
|
||||
return { success: true };
|
||||
});
|
||||
@@ -105,9 +108,31 @@ describe('Login page', () => {
|
||||
expect(screen.getByRole('tab', { name: '验证码登录' })).toHaveAttribute('aria-selected', 'false');
|
||||
expect(screen.getByLabelText('用户名')).toHaveAttribute('autocomplete', 'username');
|
||||
expect(screen.getByLabelText('密码')).toHaveAttribute('autocomplete', 'current-password');
|
||||
expect(await screen.findByRole('checkbox', { name: '记住密码' })).toBeEnabled();
|
||||
expect(screen.queryByText(/浏览器|微信|注册/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('restores OS-protected password credentials and keeps remember password selected', async () => {
|
||||
hostApiFetchMock.mockImplementation(async (path: string) => {
|
||||
if (path === '/api/auth/public-config') return { success: true, links: {} };
|
||||
if (path === '/api/auth/remembered-password') {
|
||||
return {
|
||||
success: true,
|
||||
available: true,
|
||||
credentials: { username: 'remembered-user', password: 'remembered-secret' },
|
||||
};
|
||||
}
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
renderLogin();
|
||||
|
||||
await waitFor(() => expect(screen.getByLabelText('用户名')).toHaveValue('remembered-user'));
|
||||
expect(screen.getByLabelText('密码')).toHaveValue('remembered-secret');
|
||||
expect(screen.getByRole('checkbox', { name: '记住密码' })).toBeChecked();
|
||||
expect(window.localStorage.getItem('niancode-auth')).not.toContain('remembered-secret');
|
||||
});
|
||||
|
||||
it('renders only safe projected links and degrades unavailable or unsafe links to plain text', async () => {
|
||||
hostApiFetchMock.mockResolvedValueOnce({
|
||||
success: true,
|
||||
@@ -120,8 +145,9 @@ describe('Login page', () => {
|
||||
renderLogin();
|
||||
|
||||
await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/auth/public-config', { cache: 'no-store' }));
|
||||
expect(screen.getByRole('checkbox').closest('label')).toHaveTextContent('用户协议');
|
||||
expect(screen.getByRole('checkbox').closest('label')).toHaveTextContent('隐私政策');
|
||||
const agreement = screen.getByRole('checkbox', { name: /我已阅读并同意/ });
|
||||
expect(agreement.closest('label')).toHaveTextContent('用户协议');
|
||||
expect(agreement.closest('label')).toHaveTextContent('隐私政策');
|
||||
expect(screen.queryByText('忘记密码?')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('link')).not.toBeInTheDocument();
|
||||
});
|
||||
@@ -144,14 +170,19 @@ describe('Login page', () => {
|
||||
|
||||
fireEvent.change(screen.getByLabelText('用户名'), { target: { value: ' zhangsan ' } });
|
||||
fireEvent.change(screen.getByLabelText('密码'), { target: { value: 'secret-password' } });
|
||||
fireEvent.click(await screen.findByRole('checkbox', { name: '记住密码' }));
|
||||
const submit = screen.getByRole('button', { name: '登录' });
|
||||
expect(submit).toBeDisabled();
|
||||
fireEvent.click(screen.getByRole('checkbox'));
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: /我已阅读并同意/ }));
|
||||
expect(submit).toBeEnabled();
|
||||
fireEvent.click(submit);
|
||||
|
||||
await screen.findByText('Module Selection');
|
||||
expect(loginWithPassword).toHaveBeenCalledWith({ username: 'zhangsan', password: 'secret-password' });
|
||||
expect(loginWithPassword).toHaveBeenCalledWith({
|
||||
username: 'zhangsan',
|
||||
password: 'secret-password',
|
||||
rememberPassword: true,
|
||||
});
|
||||
expect(importUserModelConfig).toHaveBeenCalledWith('password-access-token');
|
||||
});
|
||||
|
||||
@@ -166,7 +197,7 @@ describe('Login page', () => {
|
||||
expect(screen.getByLabelText('短信验证码')).toHaveAttribute('autocomplete', 'one-time-code');
|
||||
fireEvent.change(screen.getByLabelText('手机号'), { target: { value: '20123456789' } });
|
||||
fireEvent.change(screen.getByLabelText('短信验证码'), { target: { value: '123456' } });
|
||||
fireEvent.click(screen.getByRole('checkbox'));
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: /我已阅读并同意/ }));
|
||||
expect(screen.getByRole('button', { name: '登录' })).toBeDisabled();
|
||||
|
||||
fireEvent.change(screen.getByLabelText('手机号'), { target: { value: '13800138000' } });
|
||||
@@ -327,7 +358,7 @@ describe('Login page', () => {
|
||||
renderLogin();
|
||||
fireEvent.change(screen.getByLabelText('用户名'), { target: { value: 'zhangsan' } });
|
||||
fireEvent.change(screen.getByLabelText('密码'), { target: { value: 'secret-password' } });
|
||||
fireEvent.click(screen.getByRole('checkbox'));
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: /我已阅读并同意/ }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '登录' }));
|
||||
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent('登录失败,请稍后重试。');
|
||||
|
||||
Reference in new issue
Block a user