auth
This commit is contained in:
@@ -32,7 +32,8 @@ import Heading from '@/components/ui/Heading';
|
||||
import { useAuth } from '@/lib/auth/AuthContext';
|
||||
|
||||
interface FormErrors {
|
||||
displayName?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
confirmPassword?: string;
|
||||
@@ -101,7 +102,8 @@ export default function SignupPage() {
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [errors, setErrors] = useState<FormErrors>({});
|
||||
const [formData, setFormData] = useState({
|
||||
displayName: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
@@ -138,10 +140,32 @@ export default function SignupPage() {
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: FormErrors = {};
|
||||
|
||||
if (!formData.displayName.trim()) {
|
||||
newErrors.displayName = 'Display name is required';
|
||||
} else if (formData.displayName.trim().length < 3) {
|
||||
newErrors.displayName = 'Display name must be at least 3 characters';
|
||||
// First name validation
|
||||
const firstName = formData.firstName.trim();
|
||||
if (!firstName) {
|
||||
newErrors.firstName = 'First name is required';
|
||||
} else if (firstName.length < 2) {
|
||||
newErrors.firstName = 'First name must be at least 2 characters';
|
||||
} else if (firstName.length > 25) {
|
||||
newErrors.firstName = 'First name must be no more than 25 characters';
|
||||
} else if (!/^[A-Za-z\-']+$/.test(firstName)) {
|
||||
newErrors.firstName = 'First name can only contain letters, hyphens, and apostrophes';
|
||||
} else if (/^(user|test|demo|guest|player)/i.test(firstName)) {
|
||||
newErrors.firstName = 'Please use your real first name, not a nickname';
|
||||
}
|
||||
|
||||
// Last name validation
|
||||
const lastName = formData.lastName.trim();
|
||||
if (!lastName) {
|
||||
newErrors.lastName = 'Last name is required';
|
||||
} else if (lastName.length < 2) {
|
||||
newErrors.lastName = 'Last name must be at least 2 characters';
|
||||
} else if (lastName.length > 25) {
|
||||
newErrors.lastName = 'Last name must be no more than 25 characters';
|
||||
} else if (!/^[A-Za-z\-']+$/.test(lastName)) {
|
||||
newErrors.lastName = 'Last name can only contain letters, hyphens, and apostrophes';
|
||||
} else if (/^(user|test|demo|guest|player)/i.test(lastName)) {
|
||||
newErrors.lastName = 'Please use your real last name, not a nickname';
|
||||
}
|
||||
|
||||
if (!formData.email.trim()) {
|
||||
@@ -150,10 +174,13 @@ export default function SignupPage() {
|
||||
newErrors.email = 'Invalid email format';
|
||||
}
|
||||
|
||||
// Password strength validation
|
||||
if (!formData.password) {
|
||||
newErrors.password = 'Password is required';
|
||||
} else if (formData.password.length < 8) {
|
||||
newErrors.password = 'Password must be at least 8 characters';
|
||||
} else if (!/[a-z]/.test(formData.password) || !/[A-Z]/.test(formData.password) || !/\d/.test(formData.password)) {
|
||||
newErrors.password = 'Password must contain uppercase, lowercase, and number';
|
||||
}
|
||||
|
||||
if (!formData.confirmPassword) {
|
||||
@@ -176,21 +203,18 @@ export default function SignupPage() {
|
||||
setErrors({});
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/signup', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
displayName: formData.displayName,
|
||||
}),
|
||||
const { ServiceFactory } = await import('@/lib/services/ServiceFactory');
|
||||
const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001');
|
||||
const authService = serviceFactory.createAuthService();
|
||||
|
||||
// Combine first and last name into display name
|
||||
const displayName = `${formData.firstName} ${formData.lastName}`.trim();
|
||||
|
||||
await authService.signup({
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
displayName,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Signup failed');
|
||||
}
|
||||
|
||||
// Refresh session in context so header updates immediately
|
||||
await refreshSession();
|
||||
@@ -206,8 +230,12 @@ export default function SignupPage() {
|
||||
const handleDemoLogin = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// Demo: Set cookie to indicate driver mode (works without OAuth)
|
||||
document.cookie = 'gridpilot_demo_mode=driver; path=/; max-age=86400';
|
||||
const { ServiceFactory } = await import('@/lib/services/ServiceFactory');
|
||||
const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001');
|
||||
const authService = serviceFactory.createAuthService();
|
||||
|
||||
await authService.demoLogin({ role: 'driver' });
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
router.push(returnTo === '/onboarding' ? '/dashboard' : returnTo);
|
||||
} catch {
|
||||
@@ -337,27 +365,57 @@ export default function SignupPage() {
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl from-primary-blue/10 to-transparent rounded-bl-full" />
|
||||
|
||||
<form onSubmit={handleSubmit} className="relative space-y-4">
|
||||
{/* Display Name */}
|
||||
{/* First Name */}
|
||||
<div>
|
||||
<label htmlFor="displayName" className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Display Name
|
||||
<label htmlFor="firstName" className="block text-sm font-medium text-gray-300 mb-2">
|
||||
First Name
|
||||
</label>
|
||||
<div className="relative">
|
||||
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
||||
<Input
|
||||
id="displayName"
|
||||
id="firstName"
|
||||
type="text"
|
||||
value={formData.displayName}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setFormData({ ...formData, displayName: e.target.value })}
|
||||
error={!!errors.displayName}
|
||||
errorMessage={errors.displayName}
|
||||
placeholder="SpeedyRacer42"
|
||||
value={formData.firstName}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setFormData({ ...formData, firstName: e.target.value })}
|
||||
error={!!errors.firstName}
|
||||
errorMessage={errors.firstName}
|
||||
placeholder="John"
|
||||
disabled={loading}
|
||||
className="pl-10"
|
||||
autoComplete="username"
|
||||
autoComplete="given-name"
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-gray-500">This is how other drivers will see you</p>
|
||||
</div>
|
||||
|
||||
{/* Last Name */}
|
||||
<div>
|
||||
<label htmlFor="lastName" className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Last Name
|
||||
</label>
|
||||
<div className="relative">
|
||||
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
||||
<Input
|
||||
id="lastName"
|
||||
type="text"
|
||||
value={formData.lastName}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setFormData({ ...formData, lastName: e.target.value })}
|
||||
error={!!errors.lastName}
|
||||
errorMessage={errors.lastName}
|
||||
placeholder="Smith"
|
||||
disabled={loading}
|
||||
className="pl-10"
|
||||
autoComplete="family-name"
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-gray-500">Your name will be used as-is and cannot be changed later</p>
|
||||
</div>
|
||||
|
||||
{/* Name Immutability Warning */}
|
||||
<div className="flex items-start gap-3 p-3 rounded-lg bg-warning-amber/10 border border-warning-amber/30">
|
||||
<AlertCircle className="w-5 h-5 text-warning-amber flex-shrink-0 mt-0.5" />
|
||||
<div className="text-sm text-warning-amber">
|
||||
<strong>Important:</strong> Your name cannot be changed after signup. Please ensure it's correct.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
@@ -529,7 +587,7 @@ export default function SignupPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* iRacing Signup */}
|
||||
{/* Demo Login */}
|
||||
<motion.button
|
||||
type="button"
|
||||
onClick={handleDemoLogin}
|
||||
@@ -539,7 +597,7 @@ export default function SignupPage() {
|
||||
className="w-full flex items-center justify-center gap-3 px-4 py-3 rounded-lg bg-gradient-to-r from-deep-graphite to-iron-gray border border-charcoal-outline text-gray-300 hover:border-primary-blue/30 transition-all disabled:opacity-50 group"
|
||||
>
|
||||
<Gamepad2 className="w-5 h-5 text-primary-blue" />
|
||||
<span>Demo Login (iRacing)</span>
|
||||
<span>Demo Login</span>
|
||||
<ChevronRight className="w-4 h-4 text-gray-500 group-hover:translate-x-0.5 transition-transform" />
|
||||
</motion.button>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user