diff --git a/backend/src/controllers/auth.js b/backend/src/controllers/auth.js index 83fe035..74ea314 100644 --- a/backend/src/controllers/auth.js +++ b/backend/src/controllers/auth.js @@ -660,6 +660,50 @@ async function resetPassword(req, res, next) { } } +// Check username/email availability (for real-time validation) +async function checkAvailability(req, res, next) { + try { + const { username, email } = req.query; + + if (!username && !email) { + return res.status(400).json({ + success: false, + error: 'Username or email is required', + }); + } + + const result = { + usernameAvailable: true, + emailAvailable: true, + }; + + // Check username availability + if (username) { + const existingUsername = await prisma.user.findUnique({ + where: { username }, + select: { id: true }, + }); + result.usernameAvailable = !existingUsername; + } + + // Check email availability + if (email) { + const existingEmail = await prisma.user.findUnique({ + where: { email }, + select: { id: true }, + }); + result.emailAvailable = !existingEmail; + } + + res.json({ + success: true, + data: result, + }); + } catch (error) { + next(error); + } +} + module.exports = { register, login, @@ -668,4 +712,5 @@ module.exports = { resendVerification, requestPasswordReset, resetPassword, + checkAvailability, }; diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index a524a49..f535cb7 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -6,7 +6,8 @@ const { verifyEmailByCode, resendVerification, requestPasswordReset, - resetPassword + resetPassword, + checkAvailability } = require('../controllers/auth'); const { registerValidation, @@ -39,4 +40,7 @@ router.post('/request-password-reset', emailLimiter, requestPasswordReset); // POST /api/auth/reset-password - Reset password with token router.post('/reset-password', passwordResetValidation, resetPassword); +// GET /api/auth/check-availability?username=xxx&email=xxx - Check username/email availability +router.get('/check-availability', checkAvailability); + module.exports = router; diff --git a/frontend/src/pages/RegisterPage.jsx b/frontend/src/pages/RegisterPage.jsx index 19657cc..6f872e7 100644 --- a/frontend/src/pages/RegisterPage.jsx +++ b/frontend/src/pages/RegisterPage.jsx @@ -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 /> - +
+ + {formData.username.length >= 3 && ( +
+ {checkingUsername ? ( + + + Checking availability... + + ) : usernameAvailable === false ? ( + + + Username is already taken + + ) : usernameAvailable === true ? ( + + + Username is available + + ) : null} +
+ )} +
- +
+ + {formData.email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email) && ( +
+ {checkingEmail ? ( + + + Checking availability... + + ) : emailAvailable === false ? ( + + + Email is already registered + + ) : emailAvailable === true ? ( + + + Email is available + + ) : null} +
+ )} +