fix: registration form — phone mask, required fields, snake_case body
- Address and phone now required (frontend + backend) - Phone input mask: +7 (XXX) XXX-XX-XX - Fix Bad Request: register body now sends snake_case keys (req() converts camelCase→snake_case, backend schema updated to match) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -89,11 +89,11 @@ const auth: FastifyPluginAsync = async (fastify) => {
|
|||||||
// ── POST /api/auth/register ────────────────────────────────────────────────
|
// ── POST /api/auth/register ────────────────────────────────────────────────
|
||||||
fastify.post<{
|
fastify.post<{
|
||||||
Body: {
|
Body: {
|
||||||
hotelName: string
|
hotel_name: string
|
||||||
address?: string
|
address: string
|
||||||
contact: string
|
contact: string
|
||||||
email: string
|
email: string
|
||||||
phone?: string
|
phone: string
|
||||||
password: string
|
password: string
|
||||||
}
|
}
|
||||||
}>(
|
}>(
|
||||||
@@ -102,20 +102,20 @@ const auth: FastifyPluginAsync = async (fastify) => {
|
|||||||
schema: {
|
schema: {
|
||||||
body: {
|
body: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
required: ['hotelName', 'contact', 'email', 'password'],
|
required: ['hotel_name', 'address', 'contact', 'email', 'phone', 'password'],
|
||||||
properties: {
|
properties: {
|
||||||
hotelName: { type: 'string', minLength: 2 },
|
hotel_name: { type: 'string', minLength: 2 },
|
||||||
address: { type: 'string' },
|
address: { type: 'string', minLength: 2 },
|
||||||
contact: { type: 'string', minLength: 2 },
|
contact: { type: 'string', minLength: 2 },
|
||||||
email: { type: 'string' },
|
email: { type: 'string' },
|
||||||
phone: { type: 'string' },
|
phone: { type: 'string', minLength: 5 },
|
||||||
password: { type: 'string', minLength: 8 },
|
password: { type: 'string', minLength: 8 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (request, reply) => {
|
async (request, reply) => {
|
||||||
const { hotelName, address, contact, email, phone, password } = request.body
|
const { hotel_name: hotelName, address, contact, email, phone, password } = request.body
|
||||||
|
|
||||||
if (!/[a-zA-Zа-яА-ЯёЁ]/.test(password)) {
|
if (!/[a-zA-Zа-яА-ЯёЁ]/.test(password)) {
|
||||||
return reply.code(400).send({ error: 'Пароль должен содержать хотя бы одну букву' })
|
return reply.code(400).send({ error: 'Пароль должен содержать хотя бы одну букву' })
|
||||||
|
|||||||
@@ -175,13 +175,21 @@ export const api = {
|
|||||||
|
|
||||||
register: (data: {
|
register: (data: {
|
||||||
hotelName: string
|
hotelName: string
|
||||||
address?: string
|
address: string
|
||||||
contact: string
|
contact: string
|
||||||
email: string
|
email: string
|
||||||
phone?: string
|
phone: string
|
||||||
password: string
|
password: string
|
||||||
}) =>
|
}) =>
|
||||||
req<{ ok: boolean; message: string }>('POST', '/api/auth/register', data),
|
// Snake_case вручную — req() конвертирует camelCase→snake_case автоматически
|
||||||
|
req<{ ok: boolean; message: string }>('POST', '/api/auth/register', {
|
||||||
|
hotel_name: data.hotelName,
|
||||||
|
address: data.address,
|
||||||
|
contact: data.contact,
|
||||||
|
email: data.email,
|
||||||
|
phone: data.phone,
|
||||||
|
password: data.password,
|
||||||
|
}),
|
||||||
|
|
||||||
forgotPassword: (email: string) =>
|
forgotPassword: (email: string) =>
|
||||||
req<{ ok: boolean }>('POST', '/api/auth/forgot-password', { email }),
|
req<{ ok: boolean }>('POST', '/api/auth/forgot-password', { email }),
|
||||||
|
|||||||
@@ -285,6 +285,21 @@ function ForgotPasswordForm({ onBack }: { onBack: () => void }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Phone mask helper ──────────────────────────────────────────────────────────
|
||||||
|
function formatPhone(raw: string): string {
|
||||||
|
const digits = raw.replace(/\D/g, '')
|
||||||
|
// Normalize: leading 8 or 7 → keep as +7, otherwise prepend 7
|
||||||
|
const d = digits.startsWith('8') ? '7' + digits.slice(1)
|
||||||
|
: digits.startsWith('7') ? digits
|
||||||
|
: digits.length ? '7' + digits : ''
|
||||||
|
let result = '+7'
|
||||||
|
if (d.length > 1) result += ' (' + d.slice(1, Math.min(4, d.length))
|
||||||
|
if (d.length >= 4) result += ') ' + d.slice(4, Math.min(7, d.length))
|
||||||
|
if (d.length >= 7) result += '-' + d.slice(7, Math.min(9, d.length))
|
||||||
|
if (d.length >= 9) result += '-' + d.slice(9, Math.min(11, d.length))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// ── Register form ──────────────────────────────────────────────────────────────
|
// ── Register form ──────────────────────────────────────────────────────────────
|
||||||
function RegisterForm({ onSwitch }: { onSwitch: () => void }) {
|
function RegisterForm({ onSwitch }: { onSwitch: () => void }) {
|
||||||
const [done, setDone] = useState(false)
|
const [done, setDone] = useState(false)
|
||||||
@@ -300,13 +315,23 @@ function RegisterForm({ onSwitch }: { onSwitch: () => void }) {
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||||
|
|
||||||
|
const handlePhoneChange = (raw: string) => {
|
||||||
|
// Allow clearing
|
||||||
|
if (raw === '' || raw === '+') { setPhone(''); return }
|
||||||
|
setPhone(formatPhone(raw))
|
||||||
|
setErrors(prev => ({ ...prev, phone: '' }))
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const err: Record<string, string> = {}
|
const err: Record<string, string> = {}
|
||||||
if (!hotelName.trim()) err.hotelName = 'Введите название отеля'
|
if (!hotelName.trim()) err.hotelName = 'Введите название отеля'
|
||||||
|
if (!address.trim()) err.address = 'Введите адрес отеля'
|
||||||
if (!contact.trim()) err.contact = 'Введите контактное лицо'
|
if (!contact.trim()) err.contact = 'Введите контактное лицо'
|
||||||
if (!email.trim()) err.email = 'Введите email'
|
if (!email.trim()) err.email = 'Введите email'
|
||||||
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) err.email = 'Некорректный email'
|
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) err.email = 'Некорректный email'
|
||||||
|
const phoneDigits = phone.replace(/\D/g, '')
|
||||||
|
if (phoneDigits.length < 11) err.phone = 'Введите полный номер телефона'
|
||||||
if (password.length < 8) err.password = 'Минимум 8 символов'
|
if (password.length < 8) err.password = 'Минимум 8 символов'
|
||||||
else if (!/[a-zA-Zа-яА-ЯёЁ]/.test(password)) err.password = 'Пароль должен содержать хотя бы одну букву'
|
else if (!/[a-zA-Zа-яА-ЯёЁ]/.test(password)) err.password = 'Пароль должен содержать хотя бы одну букву'
|
||||||
if (!agreed) err.agreed = 'Необходимо принять пользовательское соглашение'
|
if (!agreed) err.agreed = 'Необходимо принять пользовательское соглашение'
|
||||||
@@ -366,12 +391,13 @@ function RegisterForm({ onSwitch }: { onSwitch: () => void }) {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="Адрес" error={errors.address}>
|
<Field label="Адрес *" error={errors.address}>
|
||||||
<input
|
<input
|
||||||
type="text" className="input"
|
type="text"
|
||||||
|
className={cn('input', errors.address && 'border-red-400 focus:ring-red-400')}
|
||||||
placeholder="г. Москва, ул. Примерная, д. 1"
|
placeholder="г. Москва, ул. Примерная, д. 1"
|
||||||
value={address}
|
value={address}
|
||||||
onChange={e => setAddress(e.target.value)}
|
onChange={e => { setAddress(e.target.value); setErrors(prev => ({ ...prev, address: '' })) }}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
@@ -396,12 +422,16 @@ function RegisterForm({ onSwitch }: { onSwitch: () => void }) {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="Телефон" error={errors.phone}>
|
<Field label="Телефон *" error={errors.phone}>
|
||||||
<input
|
<input
|
||||||
type="tel" className="input"
|
type="tel"
|
||||||
|
className={cn('input', errors.phone && 'border-red-400 focus:ring-red-400')}
|
||||||
placeholder="+7 (999) 000-00-00"
|
placeholder="+7 (999) 000-00-00"
|
||||||
value={phone}
|
value={phone}
|
||||||
onChange={e => setPhone(e.target.value)}
|
onChange={e => handlePhoneChange(e.target.value)}
|
||||||
|
onFocus={() => { if (!phone) setPhone('+7 (') }}
|
||||||
|
onBlur={() => { if (phone === '+7 (' || phone === '+7') setPhone('') }}
|
||||||
|
inputMode="tel"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user