220 lines
6.8 KiB
React
220 lines
6.8 KiB
React
|
|
import { useState, useEffect } from 'react'
|
|||
|
|
import { useSearchParams, useNavigate } from 'react-router-dom'
|
|||
|
|
import { verifyMagicLink, setPassword } from '../utils/api'
|
|||
|
|
import { X } from 'lucide-react'
|
|||
|
|
import './VerifyEmail.css'
|
|||
|
|
|
|||
|
|
export default function VerifyEmail() {
|
|||
|
|
const [searchParams] = useSearchParams()
|
|||
|
|
const navigate = useNavigate()
|
|||
|
|
const token = searchParams.get('token')
|
|||
|
|
|
|||
|
|
const [loading, setLoading] = useState(true)
|
|||
|
|
const [requiresPassword, setRequiresPassword] = useState(false)
|
|||
|
|
const [error, setError] = useState('')
|
|||
|
|
const [formData, setFormData] = useState({
|
|||
|
|
password: '',
|
|||
|
|
confirmPassword: '',
|
|||
|
|
username: '',
|
|||
|
|
firstName: ''
|
|||
|
|
})
|
|||
|
|
const [submitting, setSubmitting] = useState(false)
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (!token) {
|
|||
|
|
setError('Токен не указан')
|
|||
|
|
setLoading(false)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const checkToken = async () => {
|
|||
|
|
try {
|
|||
|
|
const result = await verifyMagicLink(token)
|
|||
|
|
if (result.requiresPassword) {
|
|||
|
|
setRequiresPassword(true)
|
|||
|
|
} else {
|
|||
|
|
// Уже авторизован, перенаправляем
|
|||
|
|
window.location.href = '/feed'
|
|||
|
|
}
|
|||
|
|
} catch (err) {
|
|||
|
|
setError(err.response?.data?.error || 'Неверная или устаревшая ссылка')
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
checkToken()
|
|||
|
|
}, [token])
|
|||
|
|
|
|||
|
|
const handleSubmit = async (e) => {
|
|||
|
|
e.preventDefault()
|
|||
|
|
setError('')
|
|||
|
|
|
|||
|
|
// Валидация
|
|||
|
|
if (!formData.password || formData.password.length < 8) {
|
|||
|
|
setError('Пароль должен быть не менее 8 символов')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (formData.password !== formData.confirmPassword) {
|
|||
|
|
setError('Пароли не совпадают')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!formData.username || formData.username.trim().length < 3) {
|
|||
|
|
setError('Юзернейм должен быть не менее 3 символов')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!formData.firstName || formData.firstName.trim().length < 1) {
|
|||
|
|
setError('Никнейм обязателен')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
setSubmitting(true)
|
|||
|
|
await setPassword(
|
|||
|
|
token,
|
|||
|
|
formData.password,
|
|||
|
|
formData.username.trim().toLowerCase(),
|
|||
|
|
formData.firstName.trim()
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Успешная регистрация, перенаправляем
|
|||
|
|
window.location.href = '/feed'
|
|||
|
|
} catch (err) {
|
|||
|
|
setError(err.response?.data?.error || 'Ошибка регистрации')
|
|||
|
|
} finally {
|
|||
|
|
setSubmitting(false)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (loading) {
|
|||
|
|
return (
|
|||
|
|
<div className="verify-email-page">
|
|||
|
|
<div className="verify-email-container">
|
|||
|
|
<div className="spinner" />
|
|||
|
|
<p>Проверка ссылки...</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (error && !requiresPassword) {
|
|||
|
|
return (
|
|||
|
|
<div className="verify-email-page">
|
|||
|
|
<div className="verify-email-container">
|
|||
|
|
<div className="error-state">
|
|||
|
|
<h2>Ошибка</h2>
|
|||
|
|
<p>{error}</p>
|
|||
|
|
<button className="btn-primary" onClick={() => navigate('/feed')}>
|
|||
|
|
На главную
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (requiresPassword) {
|
|||
|
|
return (
|
|||
|
|
<div className="verify-email-page">
|
|||
|
|
<div className="verify-email-container">
|
|||
|
|
<div className="verify-email-card">
|
|||
|
|
<h2>Завершение регистрации</h2>
|
|||
|
|
<p className="verify-email-subtitle">Установите пароль и заполните данные профиля</p>
|
|||
|
|
|
|||
|
|
<form onSubmit={handleSubmit}>
|
|||
|
|
<div className="form-group">
|
|||
|
|
<label>Юзернейм *</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
className="form-input"
|
|||
|
|
placeholder="username"
|
|||
|
|
value={formData.username}
|
|||
|
|
onChange={e => {
|
|||
|
|
setFormData({ ...formData, username: e.target.value.replace(/[^a-z0-9_]/gi, '').toLowerCase() })
|
|||
|
|
setError('')
|
|||
|
|
}}
|
|||
|
|
disabled={submitting}
|
|||
|
|
required
|
|||
|
|
minLength={3}
|
|||
|
|
maxLength={20}
|
|||
|
|
/>
|
|||
|
|
<p className="form-hint">Только латинские буквы, цифры и _. Нельзя изменить после регистрации</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="form-group">
|
|||
|
|
<label>Никнейм *</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
className="form-input"
|
|||
|
|
placeholder="Ваше имя"
|
|||
|
|
value={formData.firstName}
|
|||
|
|
onChange={e => {
|
|||
|
|
setFormData({ ...formData, firstName: e.target.value })
|
|||
|
|
setError('')
|
|||
|
|
}}
|
|||
|
|
disabled={submitting}
|
|||
|
|
required
|
|||
|
|
maxLength={50}
|
|||
|
|
/>
|
|||
|
|
<p className="form-hint">Можно изменить в настройках профиля</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="form-group">
|
|||
|
|
<label>Пароль *</label>
|
|||
|
|
<input
|
|||
|
|
type="password"
|
|||
|
|
className="form-input"
|
|||
|
|
placeholder="Минимум 8 символов"
|
|||
|
|
value={formData.password}
|
|||
|
|
onChange={e => {
|
|||
|
|
setFormData({ ...formData, password: e.target.value })
|
|||
|
|
setError('')
|
|||
|
|
}}
|
|||
|
|
disabled={submitting}
|
|||
|
|
required
|
|||
|
|
minLength={8}
|
|||
|
|
maxLength={24}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="form-group">
|
|||
|
|
<label>Подтвердите пароль *</label>
|
|||
|
|
<input
|
|||
|
|
type="password"
|
|||
|
|
className="form-input"
|
|||
|
|
placeholder="Повторите пароль"
|
|||
|
|
value={formData.confirmPassword}
|
|||
|
|
onChange={e => {
|
|||
|
|
setFormData({ ...formData, confirmPassword: e.target.value })
|
|||
|
|
setError('')
|
|||
|
|
}}
|
|||
|
|
disabled={submitting}
|
|||
|
|
required
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{error && (
|
|||
|
|
<div className="error-message">{error}</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<button
|
|||
|
|
type="submit"
|
|||
|
|
className="btn-primary"
|
|||
|
|
disabled={submitting || !formData.password || !formData.username || !formData.firstName}
|
|||
|
|
>
|
|||
|
|
{submitting ? 'Регистрация...' : 'Завершить регистрацию'}
|
|||
|
|
</button>
|
|||
|
|
</form>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
|