feat(auth): add real-time username/email availability validation

Backend changes:
- Added checkAvailability endpoint (GET /api/auth/check-availability)
- Checks username and email availability in database
- Returns availability status for both fields

Frontend changes:
- Added real-time validation for username (3+ characters) and email
- Debounced API calls (500ms) to avoid excessive requests
- Visual feedback with loading spinner, success checkmark, and error icons
- Improved UX by showing availability before form submission

This prevents users from submitting forms with already-taken credentials
and provides immediate feedback during registration.
This commit is contained in:
Radosław Gierwiało
2025-12-06 19:18:21 +01:00
parent fbca0c9e94
commit 71d22cc42e
4 changed files with 174 additions and 22 deletions

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useRef } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { wsdcAPI } from '../services/api';
import { wsdcAPI, authAPI } from '../services/api';
import { Video, Mail, Lock, User, Hash, ArrowRight, ArrowLeft, Loader2, CheckCircle, XCircle, AlertCircle } from 'lucide-react';
import PasswordStrengthIndicator from '../components/common/PasswordStrengthIndicator';
import FormInput from '../components/common/FormInput';
@@ -36,6 +36,12 @@ const RegisterPage = () => {
const [turnstileToken, setTurnstileToken] = useState('');
const turnstileRef = useRef(null);
// Availability validation state
const [usernameAvailable, setUsernameAvailable] = useState(null);
const [emailAvailable, setEmailAvailable] = useState(null);
const [checkingUsername, setCheckingUsername] = useState(false);
const [checkingEmail, setCheckingEmail] = useState(false);
const { register } = useAuth();
const navigate = useNavigate();
@@ -133,6 +139,50 @@ const RegisterPage = () => {
};
}, [step]);
// Check username availability
useEffect(() => {
const checkUsername = async () => {
if (formData.username.length >= 3) {
setCheckingUsername(true);
try {
const result = await authAPI.checkAvailability(formData.username, null);
setUsernameAvailable(result.usernameAvailable);
} catch (error) {
console.error('Error checking username:', error);
} finally {
setCheckingUsername(false);
}
} else {
setUsernameAvailable(null);
}
};
const timeoutId = setTimeout(checkUsername, 500); // Debounce 500ms
return () => clearTimeout(timeoutId);
}, [formData.username]);
// Check email availability
useEffect(() => {
const checkEmail = async () => {
if (formData.email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
setCheckingEmail(true);
try {
const result = await authAPI.checkAvailability(null, formData.email);
setEmailAvailable(result.emailAvailable);
} catch (error) {
console.error('Error checking email:', error);
} finally {
setCheckingEmail(false);
}
} else {
setEmailAvailable(null);
}
};
const timeoutId = setTimeout(checkEmail, 500); // Debounce 500ms
return () => clearTimeout(timeoutId);
}, [formData.email]);
// Handle WSDC ID confirmation and continue to registration
const handleWsdcContinue = () => {
if (!wsdcPreview) {
@@ -414,27 +464,71 @@ const RegisterPage = () => {
required
/>
<FormInput
label="Username"
name="username"
type="text"
value={formData.username}
onChange={handleInputChange}
icon={User}
placeholder="john_doe"
required
/>
<div>
<FormInput
label="Username"
name="username"
type="text"
value={formData.username}
onChange={handleInputChange}
icon={User}
placeholder="john_doe"
required
/>
{formData.username.length >= 3 && (
<div className="mt-1 flex items-center gap-1 text-sm">
{checkingUsername ? (
<span className="text-gray-500 flex items-center gap-1">
<Loader2 className="w-4 h-4 animate-spin" />
Checking availability...
</span>
) : usernameAvailable === false ? (
<span className="text-red-600 flex items-center gap-1">
<XCircle className="w-4 h-4" />
Username is already taken
</span>
) : usernameAvailable === true ? (
<span className="text-green-600 flex items-center gap-1">
<CheckCircle className="w-4 h-4" />
Username is available
</span>
) : null}
</div>
)}
</div>
<FormInput
label="Email"
name="email"
type="email"
value={formData.email}
onChange={handleInputChange}
icon={Mail}
placeholder="john@example.com"
required
/>
<div>
<FormInput
label="Email"
name="email"
type="email"
value={formData.email}
onChange={handleInputChange}
icon={Mail}
placeholder="john@example.com"
required
/>
{formData.email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email) && (
<div className="mt-1 flex items-center gap-1 text-sm">
{checkingEmail ? (
<span className="text-gray-500 flex items-center gap-1">
<Loader2 className="w-4 h-4 animate-spin" />
Checking availability...
</span>
) : emailAvailable === false ? (
<span className="text-red-600 flex items-center gap-1">
<XCircle className="w-4 h-4" />
Email is already registered
</span>
) : emailAvailable === true ? (
<span className="text-green-600 flex items-center gap-1">
<CheckCircle className="w-4 h-4" />
Email is available
</span>
) : null}
</div>
)}
</div>
<div>
<FormInput

View File

@@ -182,6 +182,15 @@ export const authAPI = {
return data;
},
async checkAvailability(username = null, email = null) {
const params = new URLSearchParams();
if (username) params.append('username', username);
if (email) params.append('email', email);
const data = await fetchAPI(`/auth/check-availability?${params.toString()}`);
return data.data;
},
async updateProfile(profileData) {
const data = await fetchAPI('/users/me', {
method: 'PATCH',