diff --git a/src/lib/password.test.ts b/src/lib/password.test.ts index b1f35c6..af2ed4d 100644 --- a/src/lib/password.test.ts +++ b/src/lib/password.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { isPasswordStrong } from './password'; +import { getPasswordStrength, isPasswordStrong } from './password'; describe('isPasswordStrong', () => { it('rejects passwords shorter than eight characters', () => { @@ -13,4 +13,8 @@ describe('isPasswordStrong', () => { it('accepts passwords with a zxcvbn score above two', () => { expect(isPasswordStrong('correct horse battery staple')).toBe(true); }); + + it('returns a display label and the zxcvbn score', () => { + expect(getPasswordStrength('password1')).toMatchObject({ score: 0, isStrong: false }); + }); }); diff --git a/src/lib/password.ts b/src/lib/password.ts index a86521b..deeae8e 100644 --- a/src/lib/password.ts +++ b/src/lib/password.ts @@ -3,8 +3,28 @@ import zxcvbn from 'zxcvbn'; const MIN_PASSWORD_LENGTH = 8; const MIN_PASSWORD_SCORE = 3; +export interface PasswordStrength { + score: number; + isStrong: boolean; + label: string; +} + +const strengthLabels = ['Слишком слабый', 'Слабый', 'Ненадёжный', 'Надёжный', 'Очень надёжный']; + +export function getPasswordStrength(password: string): PasswordStrength | null { + if (!password) return null; + + const score = zxcvbn(password).score; + return { + score, + isStrong: password.length >= MIN_PASSWORD_LENGTH && score >= MIN_PASSWORD_SCORE, + label: + password.length < MIN_PASSWORD_LENGTH ? 'Минимум 8 символов' : (strengthLabels[score] ?? 'Слишком слабый'), + }; +} + export function isPasswordStrong(password: string): boolean { - return password.length >= MIN_PASSWORD_LENGTH && zxcvbn(password).score >= MIN_PASSWORD_SCORE; + return getPasswordStrength(password)?.isStrong ?? false; } export const passwordStrengthMessage = diff --git a/src/pages/LoginPage.test.tsx b/src/pages/LoginPage.test.tsx index 1e51131..ab6036b 100644 --- a/src/pages/LoginPage.test.tsx +++ b/src/pages/LoginPage.test.tsx @@ -124,6 +124,18 @@ describe('LoginPage', () => { expect(api.signup).not.toHaveBeenCalled(); }); + it('shows a dynamic password strength meter during signup', async () => { + const user = userEvent.setup(); + renderLogin(); + await user.click(screen.getByRole('tab', { name: /Регистрация/i })); + await user.type(screen.getByLabelText('Пароль'), 'password1'); + + expect(screen.getByRole('progressbar', { name: /надёжность пароля/i })).toHaveAttribute( + 'aria-valuetext', + 'Слишком слабый', + ); + }); + it('shows backend error on login failure', async () => { const user = userEvent.setup(); vi.mocked(api.login).mockRejectedValue(new ApiError('неверные данные', 401)); diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 76b7a3d..e487f99 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -7,7 +7,7 @@ import { Emblem } from '../components/Emblem'; import { Logo } from '../components/Logo'; import { Reveal } from '../components/Reveal'; import { getReferralCode } from '../lib/referral'; -import { isPasswordStrong, passwordStrengthMessage } from '../lib/password'; +import { getPasswordStrength, isPasswordStrong, passwordStrengthMessage } from '../lib/password'; type Mode = 'signin' | 'signup'; @@ -54,6 +54,7 @@ export function LoginPage() { const queryReturnTo = new URLSearchParams(location.search).get('returnTo'); const returnTo = returnPath(queryReturnTo) ?? returnPath((location.state as LocationState | null)?.returnTo) ?? '/account'; + const passwordStrength = mode === 'signup' ? getPasswordStrength(password) : null; useEffect(() => { if (isAuthenticated) navigate(returnTo, { replace: true }); @@ -205,6 +206,28 @@ export function LoginPage() { + {mode === 'signup' && passwordStrength && ( +
+
+ {[0, 1, 2, 3].map((segment) => ( + + ))} +
+ {passwordStrength.label} +
+ )} + {mode === 'signin' && (