From e59bc752b0bfea9c130508f1e4302200bb6e557d Mon Sep 17 00:00:00 2001 From: HotelSync Date: Tue, 17 Mar 2026 19:35:54 +0300 Subject: [PATCH] Add password letter requirement + labeled strength UI + auto-capitalize name fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reject pure-numeric passwords (must contain ≥1 letter) in RegisterForm, ResetPasswordPage, and backend register/reset-password handlers - Replace bare strength bars with labeled 2×2 grid: ≥8 символов, Содержит букву, Заглавная буква, Цифра - Auto-capitalize first letter of each word in "Название отеля" and "Контактное лицо" fields (autoCapitalize="words" + JS handler) Co-Authored-By: Claude Sonnet 4.6 --- backend/src/routes/auth.ts | 9 +++++++++ src/pages/LoginPage.tsx | 21 ++++++++++++++++----- src/pages/ResetPasswordPage.tsx | 15 ++++++++++++--- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 50a950b..d9c234d 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -117,6 +117,10 @@ const auth: FastifyPluginAsync = async (fastify) => { async (request, reply) => { const { hotelName, address, contact, email, phone, password } = request.body + if (!/[a-zA-Zа-яА-ЯёЁ]/.test(password)) { + return reply.code(400).send({ error: 'Пароль должен содержать хотя бы одну букву' }) + } + const { rows: existing } = await db.query( 'SELECT id FROM users WHERE email = $1', [email.toLowerCase().trim()], @@ -346,6 +350,11 @@ const auth: FastifyPluginAsync = async (fastify) => { }, async (request, reply) => { const { token, password } = request.body + + if (!/[a-zA-Zа-яА-ЯёЁ]/.test(password)) { + return reply.code(400).send({ error: 'Пароль должен содержать хотя бы одну букву' }) + } + const { rows } = await db.query( `SELECT id FROM users WHERE reset_token = $1 diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index fb0e772..d5cb015 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -330,6 +330,7 @@ function RegisterForm({ onSwitch }: { onSwitch: () => void }) { if (!email.trim()) err.email = 'Введите email' else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) err.email = 'Некорректный email' if (password.length < 8) err.password = 'Минимум 8 символов' + else if (!/[a-zA-Zа-яА-ЯёЁ]/.test(password)) err.password = 'Пароль должен содержать хотя бы одну букву' setErrors(err) if (Object.keys(err).length > 0) return setLoading(true) @@ -380,8 +381,9 @@ function RegisterForm({ onSwitch }: { onSwitch: () => void }) { type="text" className={cn('input', errors.hotelName && 'border-red-400 focus:ring-red-400')} placeholder="Grand Palace Hotel" + autoCapitalize="words" value={hotelName} - onChange={e => { setHotelName(e.target.value); setErrors(prev => ({ ...prev, hotelName: '' })) }} + onChange={e => { setHotelName(e.target.value.replace(/(?:^|\s)\S/g, c => c.toUpperCase())); setErrors(prev => ({ ...prev, hotelName: '' })) }} /> @@ -399,8 +401,9 @@ function RegisterForm({ onSwitch }: { onSwitch: () => void }) { type="text" className={cn('input', errors.contact && 'border-red-400 focus:ring-red-400')} placeholder="Иванов Иван Иванович" + autoCapitalize="words" value={contact} - onChange={e => { setContact(e.target.value); setErrors(prev => ({ ...prev, contact: '' })) }} + onChange={e => { setContact(e.target.value.replace(/(?:^|\s)\S/g, c => c.toUpperCase())); setErrors(prev => ({ ...prev, contact: '' })) }} /> @@ -438,9 +441,17 @@ function RegisterForm({ onSwitch }: { onSwitch: () => void }) { {password && ( -
- {[password.length >= 8, /[A-Z]/.test(password), /[0-9]/.test(password)].map((ok, i) => ( -
+
+ {[ + { ok: password.length >= 8, label: '≥ 8 символов' }, + { ok: /[a-zA-Zа-яА-ЯёЁ]/.test(password), label: 'Содержит букву' }, + { ok: /[A-ZА-ЯЁ]/.test(password), label: 'Заглавная буква' }, + { ok: /[0-9]/.test(password), label: 'Цифра' }, + ].map(({ ok, label }) => ( +
+
+ {label} +
))}
)} diff --git a/src/pages/ResetPasswordPage.tsx b/src/pages/ResetPasswordPage.tsx index c8e681e..c682b02 100644 --- a/src/pages/ResetPasswordPage.tsx +++ b/src/pages/ResetPasswordPage.tsx @@ -21,6 +21,7 @@ export function ResetPasswordPage() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (password.length < 8) { setError('Минимум 8 символов'); return } + if (!/[a-zA-Zа-яА-ЯёЁ]/.test(password)) { setError('Пароль должен содержать хотя бы одну букву'); return } if (password !== password2) { setError('Пароли не совпадают'); return } setError('') setLoading(true) @@ -100,9 +101,17 @@ export function ResetPasswordPage() {
{password && ( -
- {[password.length >= 8, /[A-Z]/.test(password), /[0-9]/.test(password)].map((ok, i) => ( -
+
+ {[ + { ok: password.length >= 8, label: '≥ 8 символов' }, + { ok: /[a-zA-Zа-яА-ЯёЁ]/.test(password), label: 'Содержит букву' }, + { ok: /[A-ZА-ЯЁ]/.test(password), label: 'Заглавная буква' }, + { ok: /[0-9]/.test(password), label: 'Цифра' }, + ].map(({ ok, label }) => ( +
+
+ {label} +
))}
)}