Advanced registration validation: email domain check, username blacklist, MX verification
This commit is contained in:
@@ -9,13 +9,17 @@ import { useToast } from '../hooks/useToast';
|
||||
import { useTranslation } from '../i18n';
|
||||
import { useLocalePath } from '../middleware';
|
||||
import { validateEmail } from '../utils/emailValidation';
|
||||
import { validateUsername } from '../utils/usernameBlacklist';
|
||||
import { quickEmailCheck, verifyEmailWithAPI } from '../utils/emailVerification';
|
||||
|
||||
const RegisterPage = () => {
|
||||
const { addToast } = useToast();
|
||||
const [username, setUsername] = useState('');
|
||||
const [usernameError, setUsernameError] = useState<string | null>(null);
|
||||
const [email, setEmail] = useState('');
|
||||
const [emailError, setEmailError] = useState<string | null>(null);
|
||||
const [emailSuggestion, setEmailSuggestion] = useState<string | null>(null);
|
||||
const [isVerifyingEmail, setIsVerifyingEmail] = useState(false);
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -29,17 +33,57 @@ const RegisterPage = () => {
|
||||
|
||||
const siteKey = import.meta.env.VITE_TURNSTILE_SITE_KEY;
|
||||
|
||||
// Username validation on blur
|
||||
const handleUsernameBlur = useCallback(() => {
|
||||
if (!username.trim()) {
|
||||
setUsernameError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = validateUsername(username, locale);
|
||||
setUsernameError(result.isValid ? null : result.error ?? null);
|
||||
}, [username, locale]);
|
||||
|
||||
// Email validation on blur
|
||||
const handleEmailBlur = useCallback(() => {
|
||||
const handleEmailBlur = useCallback(async () => {
|
||||
if (!email.trim()) {
|
||||
setEmailError(null);
|
||||
setEmailSuggestion(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Quick client-side check
|
||||
const quickCheck = quickEmailCheck(email);
|
||||
if (!quickCheck.valid) {
|
||||
setEmailError(quickCheck.reason || (locale === 'en' ? 'Invalid email' : 'Недействительный email'));
|
||||
setEmailSuggestion(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = validateEmail(email, locale);
|
||||
setEmailError(result.isValid ? null : result.error ?? null);
|
||||
setEmailSuggestion(result.suggestion ?? null);
|
||||
|
||||
// Optional: deep verification with API (only if basic checks pass)
|
||||
if (result.isValid) {
|
||||
setIsVerifyingEmail(true);
|
||||
try {
|
||||
const apiResult = await verifyEmailWithAPI(email, { checkMX: true });
|
||||
if (!apiResult.valid) {
|
||||
setEmailError(apiResult.error || (locale === 'en'
|
||||
? 'This email address appears to be invalid'
|
||||
: 'Этот email-адрес недействителен'));
|
||||
} else if (apiResult.disposable) {
|
||||
setEmailError(locale === 'en'
|
||||
? 'Temporary email addresses are not allowed'
|
||||
: 'Временные email-адреса не допускаются');
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Email verification failed:', err);
|
||||
} finally {
|
||||
setIsVerifyingEmail(false);
|
||||
}
|
||||
}
|
||||
}, [email, locale]);
|
||||
|
||||
// Apply email suggestion
|
||||
@@ -75,6 +119,13 @@ const RegisterPage = () => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
// Validate username
|
||||
const usernameValidation = validateUsername(username, locale);
|
||||
if (!usernameValidation.isValid) {
|
||||
setUsernameError(usernameValidation.error ?? t('auth.register.invalidEmail'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate email before submit
|
||||
const emailValidation = validateEmail(email, locale);
|
||||
if (!emailValidation.isValid) {
|
||||
@@ -127,13 +178,28 @@ const RegisterPage = () => {
|
||||
<div className="bg-white p-8 md:p-10 rounded-3xl shadow-2xl w-full max-w-md text-center">
|
||||
<h1 className="text-3xl font-bold text-gray-800 mb-6">{t('auth.register.title')}</h1>
|
||||
<form onSubmit={handleRegister}>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder={t('auth.register.usernamePlaceholder')}
|
||||
className="w-full px-5 py-3 mb-4 border border-gray-300 rounded-full focus:outline-none focus:ring-2 focus:ring-ospab-primary"
|
||||
/>
|
||||
<div className="relative mb-4">
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => {
|
||||
setUsername(e.target.value);
|
||||
setUsernameError(null);
|
||||
}}
|
||||
onBlur={handleUsernameBlur}
|
||||
placeholder={t('auth.register.usernamePlaceholder')}
|
||||
className={`w-full px-5 py-3 border rounded-full focus:outline-none focus:ring-2 ${
|
||||
usernameError
|
||||
? 'border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:ring-ospab-primary'
|
||||
}`}
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{usernameError && (
|
||||
<p className="mt-1 text-sm text-red-500 text-left px-3">{usernameError}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative mb-4">
|
||||
<input
|
||||
type="email"
|
||||
@@ -148,13 +214,22 @@ const RegisterPage = () => {
|
||||
className={`w-full px-5 py-3 border rounded-full focus:outline-none focus:ring-2 ${
|
||||
emailError
|
||||
? 'border-red-500 focus:ring-red-500'
|
||||
: isVerifyingEmail
|
||||
? 'border-yellow-400 focus:ring-yellow-400'
|
||||
: 'border-gray-300 focus:ring-ospab-primary'
|
||||
}`}
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{isVerifyingEmail && (
|
||||
<p className="mt-1 text-sm text-yellow-600 text-left px-3">
|
||||
{locale === 'en' ? 'Verifying email...' : 'Проверка email...'}
|
||||
</p>
|
||||
)}
|
||||
{emailError && (
|
||||
<p className="mt-1 text-sm text-red-500 text-left px-3">{emailError}</p>
|
||||
)}
|
||||
{emailSuggestion && (
|
||||
{emailSuggestion && !emailError && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={applySuggestion}
|
||||
@@ -190,14 +265,16 @@ const RegisterPage = () => {
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading || !turnstileToken || !!emailError}
|
||||
disabled={isLoading || !turnstileToken || !!emailError || !!usernameError || isVerifyingEmail}
|
||||
className="w-full px-5 py-3 rounded-full text-white font-bold transition-colors transform hover:scale-105 bg-ospab-primary hover:bg-ospab-accent disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isLoading ? t('auth.register.loading') : t('auth.register.submit')}
|
||||
</button>
|
||||
</form>
|
||||
{error && (
|
||||
<p className="mt-4 text-sm text-red-500">{error}</p>
|
||||
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||
<p className="text-sm text-red-600">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Social networks */}
|
||||
@@ -211,38 +288,32 @@ const RegisterPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div className="mt-6 grid grid-cols-3 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleOAuthLogin('google')}
|
||||
className="w-full flex items-center justify-center px-3 py-2 border border-gray-300 rounded-lg shadow-sm bg-white text-xs sm:text-sm font-medium text-gray-700 hover:bg-gray-50 transition"
|
||||
className="flex items-center justify-center h-12 rounded-full border border-gray-300 bg-white shadow-sm hover:bg-gray-50 transition"
|
||||
aria-label={locale === 'en' ? 'Sign up with Google' : 'Регистрация через Google'}
|
||||
>
|
||||
<img src="/google.png" alt="Google" className="h-5 w-5 mr-1 sm:mr-2 flex-shrink-0" />
|
||||
<span className="truncate">Google</span>
|
||||
<img src="/google.png" alt="Google" className="h-6 w-6" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleOAuthLogin('github')}
|
||||
className="w-full flex items-center justify-center px-3 py-2 border border-gray-300 rounded-lg shadow-sm bg-white text-xs sm:text-sm font-medium text-gray-700 hover:bg-gray-50 transition"
|
||||
className="flex items-center justify-center h-12 rounded-full border border-gray-300 bg-white shadow-sm hover:bg-gray-50 transition"
|
||||
aria-label={locale === 'en' ? 'Sign up with GitHub' : 'Регистрация через GitHub'}
|
||||
>
|
||||
<img src="/github.png" alt="GitHub" className="h-5 w-5 mr-1 sm:mr-2 flex-shrink-0" />
|
||||
<span className="truncate">GitHub</span>
|
||||
<img src="/github.png" alt="GitHub" className="h-6 w-6" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleOAuthLogin('yandex')}
|
||||
className="w-full flex items-center justify-center px-3 py-2 border border-gray-300 rounded-lg shadow-sm bg-white text-xs sm:text-sm font-medium text-gray-700 hover:bg-gray-50 transition"
|
||||
className="flex items-center justify-center h-12 rounded-full border border-gray-300 bg-white shadow-sm hover:bg-gray-50 transition"
|
||||
aria-label={locale === 'en' ? 'Sign up with Yandex' : 'Регистрация через Yandex'}
|
||||
>
|
||||
<svg className="h-5 w-5 mr-1 sm:mr-2 flex-shrink-0" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<rect x="4" y="4" width="40" height="40" rx="12" fill="#000000" />
|
||||
<path
|
||||
d="M25.92 11.5h-5.04c-6.16 0-9.18 2.8-9.18 8.56v3.54c0 3.36.92 5.56 2.72 6.94l7.56 6.96h3.76l-6.08-8.8c-1.32-1.08-1.96-2.9-1.96-5.6v-3.54c0-4.08 1.82-5.84 5.62-5.84h4.06c3.8 0 5.62 1.76 5.62 5.84v2.1h3.16v-2.1c0-5.76-3.08-8.56-9.24-8.56z"
|
||||
fill="#FFFFFF"
|
||||
/>
|
||||
</svg>
|
||||
<span className="truncate">Yandex</span>
|
||||
<img src="/yandex.png" alt="" className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user