'use client' import { useSignIn, useSignUp, useUser } from '@clerk/nextjs' import { Button } from '@thedaviddias/design-system/button' import { Input } from '@thedaviddias/design-system/input' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@thedaviddias/design-system/tabs' import { logger } from '@thedaviddias/logging' import { CheckCircle, Github, Mail } from 'lucide-react' import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' import { useEffect, useState } from 'react' import { Card } from '@/components/ui/card' /** * Sanitizes and validates a redirect URL to prevent open redirect vulnerabilities * * @param redirectUrl - The redirect URL from query parameters * @returns A safe relative path or '/' as fallback */ function sanitizeRedirectUrl(redirectUrl: string | null): string { if (!redirectUrl) return '/' // Remove leading/trailing whitespace const trimmed = redirectUrl.trim() if (!trimmed) return '/' // Reject URLs with protocols (http:, https:, etc.) if (/^[a-zA-Z]+:/.test(trimmed)) { logger.warn('Redirect URL contains protocol, rejecting:', { data: { url: trimmed } }) return '/' } // Reject URLs starting with // (protocol-relative URLs) if (trimmed.startsWith('//')) { logger.warn('Redirect URL is protocol-relative, rejecting:', { data: { url: trimmed } }) return '/' } // Only allow relative paths starting with / if (!trimmed.startsWith('/')) { logger.warn('Redirect URL is not a relative path, rejecting:', { data: { url: trimmed } }) return '/' } // Normalize the path: resolve dot segments and remove duplicate slashes try { // Use URL constructor to normalize the path (relative to current origin) const normalized = new URL(trimmed, 'http://localhost').pathname // Ensure it still starts with / after normalization if (!normalized.startsWith('/')) { return '/' } return normalized } catch (error) { logger.warn('Failed to normalize redirect URL, using fallback:', { data: { url: trimmed, error } }) return '/' } } export default function LoginPage() { const { user } = useUser() const { signIn, isLoaded: signInLoaded, setActive: setSignInActive } = useSignIn() const { signUp, isLoaded: signUpLoaded, setActive: setSignUpActive } = useSignUp() const router = useRouter() const searchParams = useSearchParams() const [email, setEmail] = useState('') const [isLoading, setIsLoading] = useState(false) const [verificationCode, setVerificationCode] = useState('') const [pendingVerification, setPendingVerification] = useState(false) const [error, setError] = useState('') const [authMode, setAuthMode] = useState<'signin' | 'signup' | null>(null) const [isRedirecting, setIsRedirecting] = useState(false) // Get and sanitize redirect URL from query params const redirectTo = sanitizeRedirectUrl(searchParams.get('redirect')) // If user is already signed in, redirect useEffect(() => { if (user) { router.replace(redirectTo) } }, [user, router, redirectTo]) /** * Handle email submission for authentication * * @param e - Form event */ const handleEmailSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!signIn || !signUp) return // Basic email validation const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ if (!emailRegex.test(email)) { setError('Please enter a valid email address') return } setIsLoading(true) setError('') try { // Try to sign in first const { supportedFirstFactors } = await signIn.create({ identifier: email }) // Find the email_code factor const emailFactor = supportedFirstFactors?.find(factor => factor.strategy === 'email_code') if (emailFactor && 'emailAddressId' in emailFactor) { // Send the verification code await signIn.prepareFirstFactor({ strategy: 'email_code', emailAddressId: emailFactor.emailAddressId }) setPendingVerification(true) setAuthMode('signin') } else { throw new Error('Unable to sign in. Please try again or use GitHub authentication.') } } catch (err) { // If sign in fails because user doesn't exist, try sign up if ( err && typeof err === 'object' && 'errors' in err && Array.isArray(err.errors) && err.errors?.[0]?.code === 'form_identifier_not_found' ) { try { // Create new account with ONLY email (no OAuth requirement) await signUp.create({ emailAddress: email }) // Send verification email await signUp.prepareEmailAddressVerification({ strategy: 'email_code' }) setPendingVerification(true) setAuthMode('signup') } catch (signUpErr: any) { setError(signUpErr.errors?.[0]?.message || 'Failed to create account') logger.error('Sign up error:', { data: signUpErr }) } } else { setError('Unable to sign in. Please try again.') logger.error('Sign in error:', { data: err }) } } finally { setIsLoading(false) } } /** * Handle verification code submission * * @param e - Form event */ const handleVerifyCode = async (e: React.FormEvent) => { e.preventDefault() if (!signIn || !signUp) return setIsLoading(true) setError('') try { if (authMode === 'signup' && signUp) { // Complete sign up const completeSignUp = await signUp.attemptEmailAddressVerification({ code: verificationCode }) if (completeSignUp.status === 'complete') { // Set the session active await setSignUpActive({ session: completeSignUp.createdSessionId }) setIsRedirecting(true) // Update user metadata to track auth level setTimeout(async () => { try { await fetch('/api/auth/metadata', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ authLevel: 'email_only', canSubmitPR: false, githubConnected: false }) }) } catch (error) { logger.error('Failed to update user metadata:', { data: error }) } }, 1000) router.push(redirectTo) } } else if (authMode === 'signin' && signIn) { // Complete sign in const completeSignIn = await signIn.attemptFirstFactor({ strategy: 'email_code', code: verificationCode }) if (completeSignIn.status === 'complete') { await setSignInActive({ session: completeSignIn.createdSessionId }) setIsRedirecting(true) router.push(redirectTo) } } } catch (err) { const errorMessage = err && typeof err === 'object' && 'errors' in err && Array.isArray(err.errors) && err.errors?.[0]?.message ? err.errors[0].message : 'Invalid verification code' setError(errorMessage) logger.error('Verification error:', { data: err }) } finally { setIsLoading(false) } } /** * Handle GitHub sign in * * @returns Promise */ const handleGitHubSignIn = async () => { if (!signIn) return try { await signIn.authenticateWithRedirect({ strategy: 'oauth_github', redirectUrl: '/login/callback', redirectUrlComplete: redirectTo }) } catch (error) { logger.error('GitHub authentication error:', { data: error }) setError('Failed to connect with GitHub') } } if (user || isRedirecting) { return (

Welcome to llms.txt hub!

Redirecting you to the homepage...

) } // Show verification code input if (pendingVerification) { return (

Check your email

We sent a verification code to

{email}

setVerificationCode(e.target.value.replace(/\D/g, '').slice(0, 6))} className="text-center text-2xl tracking-[0.5em] font-mono h-14 rounded-none border-2 focus:border-foreground" maxLength={6} required autoFocus autoComplete="one-time-code" />
{error &&

{error}

}
) } // Main sign in/up form - Minimal bold design return (
{/* Subtle background pattern */}
{/* Header */}

Sign in to llms.txt hub

Manage your submissions, favorites, and profile

{/* Auth Tabs */} GitHub Email {/* GitHub Tab */}

GitHub Authentication

Recommended
  • Automated PR submissions
  • Verified contributor badge
  • Direct repository integration

Connect your GitHub for the full experience

{/* Email Tab */}

Email Authentication

  • No password required
  • Submit projects via form
  • Sync favorites across devices
{ setEmail(e.target.value) setError('') }} required disabled={isLoading} autoFocus className="h-11 rounded-none border-2 focus:border-foreground" aria-invalid={!!error} /> {error &&

{error}

}

We'll send you a 6-digit code to verify your email

{/* Divider */}
or
{/* Alternative Actions */}
Submit via GitHub → | Continue browsing →
) }