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:
@@ -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 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user