Signing in showed the "Welcome back!" toast but never left /login. The session cookie was host-only on the API subdomain, so the Next middleware guard on the site origin saw no cookie and bounced /dashboard straight back to /login?redirect=/dashboard. - Add AUTH_COOKIE_DOMAIN, wiring Better Auth's crossSubDomainCookies so the cookie also reaches the site origin. Unset in dev, where localhost is single-host and must stay host-only. - Navigate after authentication with a full page load, via a shared authRedirect helper: only a top-level request carries the httpOnly cookie. Used by the login, register, magic-link and Google flows. - Show "Redirecting..." on the login and register pages and keep the submit button disabled until the browser replaces the page, instead of re-enabling it mid-navigation. - Guard against a redirect loop with a sessionStorage marker. A React ref cannot do this: the full page load resets component state. If the destination bounces back, explain it rather than navigating again. - Middleware: accept any *.session_token cookie so a cookiePrefix change cannot lock everyone out, and preserve the destination's query string. - Trust any loopback port in dev, so reaching the dev server through a forwarded port does not fail Better Auth's CSRF origin check. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
144 lines
4.8 KiB
TypeScript
144 lines
4.8 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import Link from 'next/link';
|
|
import { useLanguage } from '@/context/LanguageContext';
|
|
import { useAuth } from '@/context/AuthContext';
|
|
import Card from '@/components/ui/Card';
|
|
import Button from '@/components/ui/Button';
|
|
import Input from '@/components/ui/Input';
|
|
import GoogleSignInButton from '@/components/GoogleSignInButton';
|
|
import { redirectAfterAuth } from '@/lib/authRedirect';
|
|
import toast from 'react-hot-toast';
|
|
|
|
const REDIRECT_TO = '/dashboard';
|
|
|
|
export default function RegisterPage() {
|
|
const { t, locale: language } = useLanguage();
|
|
const { register } = useAuth();
|
|
const [loading, setLoading] = useState(false);
|
|
const [redirecting, setRedirecting] = useState(false);
|
|
const [formData, setFormData] = useState({
|
|
name: '',
|
|
email: '',
|
|
password: '',
|
|
phone: '',
|
|
});
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setLoading(true);
|
|
|
|
try {
|
|
await register(formData);
|
|
toast.success(language === 'es' ? 'Cuenta creada exitosamente!' : 'Account created successfully!');
|
|
// Deliberately leaves `loading` set: the button must stay disabled until the
|
|
// browser replaces this page.
|
|
setRedirecting(true);
|
|
redirectAfterAuth(REDIRECT_TO);
|
|
} catch (error: any) {
|
|
toast.error(error.message || t('auth.errors.emailExists'));
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="section-padding min-h-[70vh] flex items-center">
|
|
<div className="container-page">
|
|
<div className="max-w-md mx-auto">
|
|
<div className="text-center mb-8">
|
|
<h1 className="text-3xl font-bold">{t('auth.register.title')}</h1>
|
|
<p className="mt-2 text-gray-600">{t('auth.register.subtitle')}</p>
|
|
</div>
|
|
|
|
<Card className="p-8">
|
|
{/* Google Sign-In Button */}
|
|
<GoogleSignInButton
|
|
redirectTo="/dashboard"
|
|
text="signup_with"
|
|
className="mb-4"
|
|
/>
|
|
|
|
{/* Or Divider */}
|
|
<div className="relative my-6">
|
|
<div className="absolute inset-0 flex items-center">
|
|
<div className="w-full border-t border-gray-300"></div>
|
|
</div>
|
|
<div className="relative flex justify-center text-sm">
|
|
<span className="px-2 bg-white text-gray-500">
|
|
{language === 'es' ? 'o registrarse con email' : 'or register with email'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
<Input
|
|
id="name"
|
|
label={t('auth.register.name')}
|
|
value={formData.name}
|
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
required
|
|
minLength={2}
|
|
/>
|
|
|
|
<Input
|
|
id="email"
|
|
label={t('auth.register.email')}
|
|
type="email"
|
|
value={formData.email}
|
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|
required
|
|
/>
|
|
|
|
<Input
|
|
id="password"
|
|
label={t('auth.register.password')}
|
|
type="password"
|
|
value={formData.password}
|
|
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
|
required
|
|
minLength={8}
|
|
/>
|
|
|
|
<Input
|
|
id="phone"
|
|
label={t('auth.register.phone')}
|
|
type="tel"
|
|
value={formData.phone}
|
|
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
|
/>
|
|
|
|
<Button
|
|
type="submit"
|
|
className="w-full"
|
|
size="lg"
|
|
isLoading={loading || redirecting}
|
|
loadingText={redirecting ? t('auth.login.redirecting') : t('common.loading')}
|
|
>
|
|
{t('auth.register.submit')}
|
|
</Button>
|
|
|
|
{redirecting && (
|
|
<p
|
|
className="text-center text-sm text-gray-600"
|
|
role="status"
|
|
aria-live="polite"
|
|
>
|
|
{t('auth.login.redirecting')}
|
|
</p>
|
|
)}
|
|
</form>
|
|
|
|
<p className="mt-6 text-center text-sm text-gray-600">
|
|
{t('auth.register.hasAccount')}{' '}
|
|
<Link href="/login" className="text-secondary-blue hover:underline font-medium">
|
|
{t('auth.register.login')}
|
|
</Link>
|
|
</p>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|