diff --git a/src/app/(auth)/accept-invite/page.tsx b/src/app/(auth)/accept-invite/page.tsx index 4deb163..132955f 100644 --- a/src/app/(auth)/accept-invite/page.tsx +++ b/src/app/(auth)/accept-invite/page.tsx @@ -18,6 +18,52 @@ import { AnimatedCard } from '@/components/shared/animated-container' type InviteState = 'loading' | 'valid' | 'accepting' | 'error' +function ErrorRedirectCard({ + errorContent, + redirectTarget, +}: { + errorContent: { icon: React.ReactNode; title: string; description: string; redirect?: string } + redirectTarget: string +}) { + const router = useRouter() + + useEffect(() => { + const timer = setTimeout(() => { + router.push(redirectTarget) + }, 4000) + return () => clearTimeout(timer) + }, [redirectTarget, router]) + + return ( + + +
+ +
+ {errorContent.icon} +
+ {errorContent.title} + + {errorContent.description} + +
+ + +

+ Redirecting to login in a few seconds... +

+
+ + + ) +} + function AcceptInviteContent() { const [state, setState] = useState('loading') const [errorType, setErrorType] = useState(null) @@ -105,18 +151,21 @@ function AcceptInviteContent() { icon: , title: 'Invalid Invitation', description: 'This invitation link is not valid. It may have already been used or the link is incorrect.', + redirect: '/login?expired=1', } case 'EXPIRED_TOKEN': return { icon: , title: 'Invitation Expired', description: 'This invitation has expired. Please contact your administrator to receive a new invitation.', + redirect: '/login?expired=1', } case 'ALREADY_ACCEPTED': return { icon: , title: 'Already Accepted', description: 'This invitation has already been accepted. You can sign in with your credentials.', + redirect: '/login', } case 'AUTH_FAILED': return { @@ -148,34 +197,12 @@ function AcceptInviteContent() { ) } - // Error state + // Error state — auto-redirect to login after 4 seconds for known errors if (state === 'error') { const errorContent = getErrorContent() - return ( - - -
- -
- {errorContent.icon} -
- {errorContent.title} - - {errorContent.description} - -
- - - - - - ) + const redirectTarget = errorContent.redirect || '/login' + + return } // Valid invitation - show welcome diff --git a/src/app/(auth)/error/page.tsx b/src/app/(auth)/error/page.tsx index d7b950f..9f4d2f7 100644 --- a/src/app/(auth)/error/page.tsx +++ b/src/app/(auth)/error/page.tsx @@ -1,50 +1,96 @@ 'use client' -import { useSearchParams } from 'next/navigation' +import { useSearchParams, useRouter } from 'next/navigation' +import { useEffect, Suspense } from 'react' import Link from 'next/link' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Logo } from '@/components/shared/logo' -import { AlertCircle } from 'lucide-react' +import { AlertCircle, Clock, Loader2 } from 'lucide-react' import { AnimatedCard } from '@/components/shared/animated-container' const errorMessages: Record = { Configuration: 'There is a problem with the server configuration.', AccessDenied: 'You do not have access to this resource.', - Verification: 'The verification link has expired or already been used.', + Verification: 'This sign-in link has expired or has already been used. Please request a new one.', Default: 'An error occurred during authentication.', } -export default function AuthErrorPage() { +function AuthErrorContent() { const searchParams = useSearchParams() + const router = useRouter() const error = searchParams.get('error') || 'Default' const message = errorMessages[error] || errorMessages.Default + const isExpired = error === 'Verification' + + useEffect(() => { + if (isExpired) { + const timer = setTimeout(() => { + router.push('/login?expired=1') + }, 5000) + return () => clearTimeout(timer) + } + }, [isExpired, router]) return ( - -
- -
- -
-
- -
- Authentication Error -
- -

{message}

-
- - -
-
- + +
+ +
+ +
+
+ {isExpired ? ( + + ) : ( + + )} +
+ + {isExpired ? 'Link Expired' : 'Authentication Error'} + +
+ +

{message}

+ {isExpired && ( +

+ Redirecting to login in 5 seconds... +

+ )} +
+ + {!isExpired && ( + + )} +
+
+ ) } + +export default function AuthErrorPage() { + return ( + + +
+ + + + + + } + > + + + ) +} diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index 34f1626..5a907aa 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -15,7 +15,7 @@ import { CardHeader, CardTitle, } from '@/components/ui/card' -import { Mail, Loader2, CheckCircle2, AlertCircle, Lock, KeyRound } from 'lucide-react' +import { Mail, Loader2, CheckCircle2, AlertCircle, Lock, KeyRound, Clock } from 'lucide-react' import { AnimatedCard } from '@/components/shared/animated-container' type LoginMode = 'password' | 'magic-link' @@ -32,6 +32,7 @@ export default function LoginPage() { const router = useRouter() const callbackUrl = searchParams.get('callbackUrl') || '/' const errorParam = searchParams.get('error') + const isExpiredLink = searchParams.get('expired') === '1' const handlePasswordLogin = async (e: React.FormEvent) => { e.preventDefault() @@ -172,6 +173,17 @@ export default function LoginPage() { + {isExpiredLink && ( +
+ +
+

Your link has expired

+

+ Sign-in links expire after 15 minutes for security. Please sign in below or request a new magic link. +

+
+
+ )} {mode === 'password' ? ( // Password login form