components/login-form.tsx
Rama main · réplica de lectura del repositorio original.
'use client'
import { useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { signIn, signInWithGoogle, signInWithOidc } from '@/lib/actions/auth'
import { safeRedirectPath } from '@/lib/auth/redirect-target'
import { useAuthCapabilities } from '@/lib/contexts/app-user-context'
import { cn } from '@/lib/utils/index'
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '@/components/ui/card'
import { IconLogo } from '@/components/ui/icons'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { PasswordInput } from './ui/password-input'
export function LoginForm({
className,
next,
confirmed = false,
...props
}: React.ComponentPropsWithoutRef<'div'> & {
next?: string
confirmed?: boolean
}) {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
const capabilities = useAuthCapabilities()
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
setError(null)
try {
const result = await signIn({ email, password })
if (!result.success) throw new Error(result.error ?? 'An error occurred')
// Redirect to the page that required sign-in (when it is a safe
// relative path) and refresh to ensure server components get
// updated session
router.push(safeRedirectPath(next))
router.refresh()
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}
const handleSocialLogin = async () => {
setIsLoading(true)
setError(null)
try {
// Supabase's redirect allowlist matches the callback URL exactly, so
// the destination rides in a short-lived cookie, not the redirectTo.
// Always write it: a login without a destination must clear a stale
// cookie left by an abandoned attempt, or that old destination would
// hijack the later sign-in.
document.cookie = next
? `auth_next=${encodeURIComponent(
safeRedirectPath(next)
)}; path=/; max-age=300; SameSite=Lax`
: 'auth_next=; path=/; max-age=0; SameSite=Lax'
const result = await signInWithGoogle()
if (!result.success)
throw new Error(result.error ?? 'An OAuth error occurred')
if (result.redirectTo) {
window.location.href = result.redirectTo
}
} catch (error: unknown) {
setError(
error instanceof Error ? error.message : 'An OAuth error occurred'
)
} finally {
setIsLoading(false)
}
}
const handleOidcLogin = async () => {
setIsLoading(true)
setError(null)
try {
document.cookie = next
? `auth_next=${encodeURIComponent(
safeRedirectPath(next)
)}; path=/; max-age=300; SameSite=Lax`
: 'auth_next=; path=/; max-age=0; SameSite=Lax'
const result = await signInWithOidc()
if (!result.success)
throw new Error(result.error ?? 'An OAuth error occurred')
if (result.redirectTo) {
window.location.href = result.redirectTo
}
} catch (error: unknown) {
setError(
error instanceof Error ? error.message : 'An OAuth error occurred'
)
} finally {
setIsLoading(false)
}
}
return (
<div
className={cn('flex flex-col items-center gap-6', className)}
{...props}
>
<Card className="w-full max-w-sm">
<CardHeader className="text-center">
<CardTitle className="text-2xl flex flex-col items-center justify-center gap-4">
<IconLogo className="size-12" />
Welcome back
</CardTitle>
<CardDescription>
{confirmed
? 'If you just confirmed your email, sign in to continue.'
: 'Sign in to your account'}
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-4">
{capabilities.oauth && (
<>
<Button
variant="outline"
type="button"
className="w-full"
onClick={
capabilities.oauthProvider
? handleOidcLogin
: handleSocialLogin
}
disabled={isLoading}
>
{capabilities.oauthProvider
? `Sign In with ${capabilities.oauthProvider}`
: 'Sign In with Google'}
</Button>
<div className="relative my-2">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-muted px-2 text-muted-foreground">
Or
</span>
</div>
</div>
</>
)}
<form onSubmit={handleLogin} className="flex flex-col gap-4">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
required
value={email}
onChange={e => setEmail(e.target.value)}
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
{capabilities.passwordReset && (
<Link
href="/auth/forgot-password"
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
>
Forgot password?
</Link>
)}
</div>
<PasswordInput
id="password"
type="password"
placeholder="********"
required
value={password}
onChange={e => setPassword(e.target.value)}
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Sign In'}
</Button>
</form>
</div>
{capabilities.signUp && (
<div className="mt-6 text-center text-sm">
Don't have an account?{' '}
<Link
href="/auth/sign-up"
className="underline underline-offset-4"
>
Sign Up
</Link>
</div>
)}
</CardContent>
</Card>
<div className="text-center text-xs text-muted-foreground">
<Link href="/" className="hover:underline">
← Back to Home
</Link>
</div>
</div>
)
}