Add hotel registration with email confirmation (nodemailer, confirm-email endpoint)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 17:30:07 +03:00
parent c06f4bf553
commit 152a8c0463
6 changed files with 259 additions and 5 deletions

View File

@@ -145,6 +145,16 @@ export const api = {
me: () =>
req<User>('GET', '/api/auth/me'),
register: (data: {
hotelName: string
address?: string
contact: string
email: string
phone?: string
password: string
}) =>
req<{ ok: boolean; message: string }>('POST', '/api/auth/register', data),
},
// ── Rooms ─────────────────────────────────────────────────────────────────

View File

@@ -1,5 +1,5 @@
import { useState } from 'react'
import { Navigate, useNavigate } from 'react-router-dom'
import { Navigate, useNavigate, useSearchParams } from 'react-router-dom'
import {
Hotel, Eye, EyeOff, Sun, Moon, AlertCircle,
Building2, ChevronRight, ChevronLeft, CheckCircle2, User,
@@ -7,6 +7,7 @@ import {
import { useAuth } from '../contexts/AuthContext'
import { useTheme } from '../contexts/ThemeContext'
import { cn } from '../lib/utils'
import { api } from '../lib/api'
const DEMO_EMAILS = [
'manager@grand-palace.ru',
@@ -35,6 +36,9 @@ function LoginForm({ onSwitch }: { onSwitch: () => void }) {
const [showPass, setShowPass] = useState(false)
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [searchParams] = useSearchParams()
const confirmed = searchParams.get('confirmed') === '1'
const tokenError = searchParams.get('error')
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
@@ -90,6 +94,22 @@ function LoginForm({ onSwitch }: { onSwitch: () => void }) {
</div>
)}
{confirmed && (
<div className="flex items-center gap-2 p-3 rounded-lg bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-300 text-sm">
<CheckCircle2 size={15} /> Email подтверждён! Теперь вы можете войти.
</div>
)}
{tokenError === 'invalid_token' && (
<div className="flex items-center gap-2 p-3 rounded-lg bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-300 text-sm">
<AlertCircle size={15} /> Ссылка недействительна или уже использована.
</div>
)}
{tokenError === 'token_expired' && (
<div className="flex items-center gap-2 p-3 rounded-lg bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-300 text-sm">
<AlertCircle size={15} /> Ссылка истекла (24 ч). Пожалуйста, зарегистрируйтесь снова.
</div>
)}
<button type="submit" disabled={loading} className="btn-primary w-full justify-center py-2.5">
{loading
? <span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
@@ -162,9 +182,19 @@ function RegisterForm({ onSwitch }: { onSwitch: () => void }) {
setErrors(err)
if (Object.keys(err).length > 0) return
setLoading(true)
await new Promise(r => setTimeout(r, 1000))
setLoading(false)
setDone(true)
try {
await api.auth.register({ hotelName, address, contact, email, phone, password })
setDone(true)
} catch (regErr) {
const msg = regErr instanceof Error ? regErr.message : 'Ошибка регистрации'
if (msg.includes('уже существует')) {
setErrors({ email: 'Пользователь с таким email уже существует' })
} else {
setErrors({ general: msg })
}
} finally {
setLoading(false)
}
}
if (done) {
@@ -259,6 +289,12 @@ function RegisterForm({ onSwitch }: { onSwitch: () => void }) {
)}
</Field>
{errors.general && (
<div className="flex items-center gap-2 p-3 rounded-lg bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 text-sm">
<AlertCircle size={15} /> {errors.general}
</div>
)}
<button type="submit" disabled={loading} className="btn-primary w-full justify-center py-2.5 mt-2">
{loading
? <span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />