'use client'; import { useState, useEffect, Suspense } from 'react'; import Link from 'next/link'; import { useRouter, useSearchParams } from 'next/navigation'; import { Loader2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { signIn } from 'next-auth/react'; import { getSafeCallbackUrl, isInvitationCallbackUrl, isSafeRelativePath, } from '@/lib/safe-redirect'; /** * Sign-up link that carries the pending destination — and, when that destination is an * invitation, the invitation token itself so the new account is bound to the invite. */ function buildRegisterHref(callbackUrl: string): string { if (callbackUrl === '/dashboard') return '/register'; const params = new URLSearchParams({ callbackUrl }); if (isInvitationCallbackUrl(callbackUrl)) { const token = new URLSearchParams(callbackUrl.split('?')[1] ?? '').get('token'); if (token) params.set('invitationToken', token); } return `/register?${params.toString()}`; } const ERROR_MESSAGES: Record = { RegistrationClosed: 'Sign-up is currently invite-only. Contact an administrator.', // Generic message — avoid confirming whether a credentials account exists for this email OAuthAccountNotLinked: 'Sign-in failed. Please try a different method or contact support.', OAuthCallbackError: 'OAuth sign-in failed. Please try again.', OAuthEmailNotVerified: 'Your OAuth account email is not verified. Please verify it with your provider and try again.', InvalidVerificationToken: 'The verification link is invalid or has expired.', VerificationFailed: 'Email verification failed. Please try again.', Default: 'Something went wrong. Please try again.', }; interface LoginFormInnerProps { googleEnabled: boolean; githubEnabled: boolean; } function LoginFormInner({ googleEnabled, githubEnabled }: LoginFormInnerProps) { const router = useRouter(); const searchParams = useSearchParams(); const [isLoading, setIsLoading] = useState(false); const [oauthLoading, setOauthLoading] = useState(null); const [error, setError] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [showSuccess, setShowSuccess] = useState(false); const [showVerifiedSuccess, setShowVerifiedSuccess] = useState(false); const callbackUrl = getSafeCallbackUrl(searchParams.get('callbackUrl')); const isInvitationFlow = isInvitationCallbackUrl(callbackUrl); const registerHref = buildRegisterHref(callbackUrl); useEffect(() => { if (searchParams.get('registered') === 'true') { setShowSuccess(true); } if (searchParams.get('verified') === 'true') { setShowVerifiedSuccess(true); } const errorCode = searchParams.get('error'); if (errorCode) { setError(ERROR_MESSAGES[errorCode] ?? ERROR_MESSAGES.Default); } }, [searchParams]); const handleEmailLogin = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); setError(''); try { const result = await signIn('credentials', { email, password, redirect: false, callbackUrl, }); if (result?.error) { setError('Invalid email or password'); return; } // `result.url` is whatever next-auth resolved, so it is sanitized again here — and // re-checked at the sink, because `router.push` happily leaves the origin. const destination = getSafeCallbackUrl(result?.url || callbackUrl); router.push(isSafeRelativePath(destination) ? destination : '/dashboard'); router.refresh(); } catch { setError('Something went wrong. Please try again.'); } finally { setIsLoading(false); } }; const handleOAuthLogin = async (provider: string) => { setOauthLoading(provider); setError(''); await signIn(provider, { callbackUrl }); }; const hasOAuth = googleEnabled || githubEnabled; const anyLoading = isLoading || oauthLoading !== null; return ( Welcome back {isInvitationFlow ? 'Sign in to accept your invitation' : 'Sign in to your account to continue'} {/* `?registered=true` is only ever reached when email verification is off: the register page sends a user who has to verify to /verify-email instead. Telling this one to go and check a mailbox pointed them at a message that never arrives, on a self-hosted deployment without SMTP, which is the documented default. */} {showSuccess && (
Account created successfully! You can sign in now.
)} {showVerifiedSuccess && (
Email verified successfully! You can now sign in.
)} {error && (
{error}
)} {/* OAuth Buttons */} {hasOAuth && (
{googleEnabled && ( )} {githubEnabled && ( )}
)} {/* Divider */} {hasOAuth && (
or continue with email
)} {/* Email Form */}
{ setEmail(e.target.value); setError(''); }} required disabled={anyLoading} />
{ setPassword(e.target.value); setError(''); }} required disabled={anyLoading} />

Don't have an account?{' '} Sign up

); } export function LoginFormSkeleton() { return ( Welcome back Sign in to your account to continue
); } export function LoginForm({ googleEnabled, githubEnabled }: LoginFormInnerProps) { return ( }> ); }