| name | add-login |
| description | Inyectar sistema de autenticacion completo: login, signup, password reset, profiles, Google OAuth, y RLS. Activar cuando el usuario dice: necesito login, agregar registro, autenticacion, que los usuarios puedan entrar, crear cuentas, o proteger rutas. |
| allowed-tools | Read, Write, Edit, Bash |
Sistema de Autenticacion Completo
Inyecta autenticacion B2B production-ready con Supabase + Next.js 16.
NO preguntes. Ejecuta el Golden Path completo.
Contexto Tecnico
Next.js 16:
proxy.ts (no middleware.ts) - Node.js runtime
- Funcion:
proxy() (no middleware())
Supabase SSR:
@supabase/ssr con getAll() / setAll() (NUNCA get/set/remove)
- Server: siempre
getUser(), NUNCA getSession()
Patron Profiles:
auth.users es privado y limitado
public.profiles almacena datos del usuario
- Trigger crea perfil automaticamente al signup
Google OAuth:
- Supabase tiene Google OAuth built-in (NO se necesita NextAuth)
signInWithOAuth({ provider: 'google' }) maneja el redirect completo
access_type: 'offline' + prompt: 'consent' para obtener refresh tokens
- Callback route en
/callback intercambia code por sesión
- Futuro: refresh tokens permiten integrar Google Workspace (Gmail, Calendar, Sheets)
Archivos a Crear
1. proxy.ts (root)
import { NextResponse, type NextRequest } from 'next/server'
import { updateSession } from '@/lib/supabase/proxy'
export async function proxy(request: NextRequest) {
return await updateSession(request)
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
2. src/lib/supabase/proxy.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function updateSession(request: NextRequest) {
let supabaseResponse = NextResponse.next({ request })
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) =>
request.cookies.set(name, value)
)
supabaseResponse = NextResponse.next({ request })
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
)
},
},
}
)
const { data: { user } } = await supabase.auth.getUser()
const isProtectedRoute = request.nextUrl.pathname.startsWith('/dashboard')
const isAuthRoute = request.nextUrl.pathname.startsWith('/login') ||
request.nextUrl.pathname.startsWith('/signup') ||
request.nextUrl.pathname.startsWith('/callback')
if (isProtectedRoute && !user) {
return NextResponse.redirect(new URL('/login', request.url))
}
if (isAuthRoute && user) {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
return supabaseResponse
}
3. src/types/database.ts
export interface Profile {
id: string
email: string
full_name: string | null
avatar_url: string | null
created_at: string
updated_at: string
}
export interface Database {
public: {
Tables: {
profiles: {
Row: Profile
Insert: Omit<Profile, 'created_at' | 'updated_at'>
Update: Partial<Omit<Profile, 'id' | 'created_at'>>
}
}
}
}
4. src/actions/auth.ts
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'
export async function login(formData: FormData) {
const supabase = await createClient()
const { error } = await supabase.auth.signInWithPassword({
email: formData.get('email') as string,
password: formData.get('password') as string,
})
if (error) {
return { error: error.message }
}
revalidatePath('/', 'layout')
redirect('/dashboard')
}
export async function signup(formData: FormData) {
const supabase = await createClient()
const { error } = await supabase.auth.signUp({
email: formData.get('email') as string,
password: formData.get('password') as string,
})
if (error) {
return { error: error.message }
}
revalidatePath('/', 'layout')
redirect('/check-email')
}
export async function signout() {
const supabase = await createClient()
await supabase.auth.signOut()
revalidatePath('/', 'layout')
redirect('/login')
}
export async function resetPassword(formData: FormData) {
const supabase = await createClient()
const email = formData.get('email') as string
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/update-password`,
})
if (error) {
return { error: error.message }
}
return { success: true }
}
export async function updatePassword(formData: FormData) {
const supabase = await createClient()
const password = formData.get('password') as string
const { error } = await supabase.auth.updateUser({ password })
if (error) {
return { error: error.message }
}
revalidatePath('/', 'layout')
redirect('/dashboard')
}
export async function updateProfile(formData: FormData) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return { error: 'Not authenticated' }
}
const { error } = await supabase
.from('profiles')
.update({
full_name: formData.get('full_name') as string,
updated_at: new Date().toISOString(),
})
.eq('id', user.id)
if (error) {
return { error: error.message }
}
revalidatePath('/', 'layout')
return { success: true }
}
5. src/hooks/useAuth.ts
'use client'
import { useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'
import type { User } from '@supabase/supabase-js'
import type { Profile } from '@/types/database'
export function useAuth() {
const [user, setUser] = useState<User | null>(null)
const [profile, setProfile] = useState<Profile | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const supabase = createClient()
async function getProfile(userId: string) {
const { data } = await supabase
.from('profiles')
.select('*')
.eq('id', userId)
.single()
setProfile(data)
}
supabase.auth.getUser().then(({ data: { user } }) => {
setUser(user)
if (user) {
getProfile(user.id)
}
setLoading(false)
})
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(_event, session) => {
const currentUser = session?.user ?? null
setUser(currentUser)
if (currentUser) {
getProfile(currentUser.id)
} else {
setProfile(null)
}
setLoading(false)
}
)
return () => subscription.unsubscribe()
}, [])
return { user, profile, loading }
}
6. src/features/auth/components/LoginForm.tsx
'use client'
import { useState } from 'react'
import { useSearchParams } from 'next/navigation'
import Link from 'next/link'
import { login } from '@/actions/auth'
import { GoogleSignInButton } from './GoogleSignInButton'
import { AuthDivider } from './AuthDivider'
export function LoginForm() {
const searchParams = useSearchParams()
const oauthError = searchParams.get('error')
const [error, setError] = useState<string | null>(
oauthError === 'auth_callback_failed' ? 'Error al iniciar sesión con Google. Intenta de nuevo.' : null
)
const [loading, setLoading] = useState(false)
async function handleSubmit(formData: FormData) {
setLoading(true)
setError(null)
const result = await login(formData)
if (result?.error) {
setError(result.error)
setLoading(false)
}
}
return (
<div className="space-y-6">
<GoogleSignInButton />
<AuthDivider />
<form action={handleSubmit} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium">
Email
</label>
<input
id="email"
name="email"
type="email"
required
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium">
Password
</label>
<input
id="password"
name="password"
type="password"
required
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
{error && (
<p className="text-sm text-red-600">{error}</p>
)}
<button
type="submit"
disabled={loading}
className="w-full rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Signing in...' : 'Sign In'}
</button>
<p className="text-center text-sm text-gray-600">
<Link href="/forgot-password" className="text-blue-600 hover:underline">
Forgot password?
</Link>
</p>
</form>
</div>
)
}
7. src/features/auth/components/SignupForm.tsx
'use client'
import { useState } from 'react'
import { signup } from '@/actions/auth'
import { GoogleSignInButton } from './GoogleSignInButton'
import { AuthDivider } from './AuthDivider'
export function SignupForm() {
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
async function handleSubmit(formData: FormData) {
setLoading(true)
setError(null)
const result = await signup(formData)
if (result?.error) {
setError(result.error)
setLoading(false)
}
}
return (
<div className="space-y-6">
<GoogleSignInButton label="Registrarse con Google" />
<AuthDivider />
<form action={handleSubmit} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium">
Email
</label>
<input
id="email"
name="email"
type="email"
required
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium">
Password
</label>
<input
id="password"
name="password"
type="password"
required
minLength={6}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
{error && (
<p className="text-sm text-red-600">{error}</p>
)}
<button
type="submit"
disabled={loading}
className="w-full rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Creating account...' : 'Create Account'}
</button>
</form>
</div>
)
}
8. src/features/auth/components/ForgotPasswordForm.tsx
'use client'
import { useState } from 'react'
import { resetPassword } from '@/actions/auth'
export function ForgotPasswordForm() {
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState(false)
const [loading, setLoading] = useState(false)
async function handleSubmit(formData: FormData) {
setLoading(true)
setError(null)
const result = await resetPassword(formData)
if (result?.error) {
setError(result.error)
setLoading(false)
} else {
setSuccess(true)
setLoading(false)
}
}
if (success) {
return (
<div className="text-center">
<p className="text-green-600">Check your email for a reset link.</p>
</div>
)
}
return (
<form action={handleSubmit} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium">
Email
</label>
<input
id="email"
name="email"
type="email"
required
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
{error && (
<p className="text-sm text-red-600">{error}</p>
)}
<button
type="submit"
disabled={loading}
className="w-full rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Sending...' : 'Send Reset Link'}
</button>
</form>
)
}
9. src/features/auth/components/UpdatePasswordForm.tsx
'use client'
import { useState } from 'react'
import { updatePassword } from '@/actions/auth'
export function UpdatePasswordForm() {
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
async function handleSubmit(formData: FormData) {
setLoading(true)
setError(null)
const result = await updatePassword(formData)
if (result?.error) {
setError(result.error)
setLoading(false)
}
}
return (
<form action={handleSubmit} className="space-y-4">
<div>
<label htmlFor="password" className="block text-sm font-medium">
New Password
</label>
<input
id="password"
name="password"
type="password"
required
minLength={6}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
{error && (
<p className="text-sm text-red-600">{error}</p>
)}
<button
type="submit"
disabled={loading}
className="w-full rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Updating...' : 'Update Password'}
</button>
</form>
)
}
10. src/features/auth/components/index.ts
export { LoginForm } from './LoginForm'
export { SignupForm } from './SignupForm'
export { GoogleSignInButton } from './GoogleSignInButton'
export { AuthDivider } from './AuthDivider'
export { ForgotPasswordForm } from './ForgotPasswordForm'
export { UpdatePasswordForm } from './UpdatePasswordForm'
11. src/app/(auth)/login/page.tsx
import Link from 'next/link'
import { LoginForm } from '@/features/auth/components'
export default function LoginPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="w-full max-w-md space-y-8 p-8">
<div className="text-center">
<h1 className="text-3xl font-bold">Welcome back</h1>
<p className="mt-2 text-gray-600">Sign in to your account</p>
</div>
<LoginForm />
<p className="text-center text-sm text-gray-600">
Don't have an account?{' '}
<Link href="/signup" className="text-blue-600 hover:underline">
Sign up
</Link>
</p>
</div>
</div>
)
}
12. src/app/(auth)/signup/page.tsx
import Link from 'next/link'
import { SignupForm } from '@/features/auth/components'
export default function SignupPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="w-full max-w-md space-y-8 p-8">
<div className="text-center">
<h1 className="text-3xl font-bold">Create account</h1>
<p className="mt-2 text-gray-600">Get started for free</p>
</div>
<SignupForm />
<p className="text-center text-sm text-gray-600">
Already have an account?{' '}
<Link href="/login" className="text-blue-600 hover:underline">
Sign in
</Link>
</p>
</div>
</div>
)
}
13. src/app/(auth)/check-email/page.tsx
import Link from 'next/link'
export default function CheckEmailPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="w-full max-w-md space-y-8 p-8 text-center">
<h1 className="text-3xl font-bold">Check your email</h1>
<p className="text-gray-600">
We've sent you a confirmation link. Please check your email to complete your registration.
</p>
<Link
href="/login"
className="inline-block text-blue-600 hover:underline"
>
Back to login
</Link>
</div>
</div>
)
}
14. src/app/(auth)/forgot-password/page.tsx
import Link from 'next/link'
import { ForgotPasswordForm } from '@/features/auth/components'
export default function ForgotPasswordPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="w-full max-w-md space-y-8 p-8">
<div className="text-center">
<h1 className="text-3xl font-bold">Reset password</h1>
<p className="mt-2 text-gray-600">Enter your email to receive a reset link</p>
</div>
<ForgotPasswordForm />
<p className="text-center text-sm text-gray-600">
<Link href="/login" className="text-blue-600 hover:underline">
Back to login
</Link>
</p>
</div>
</div>
)
}
15. src/app/(auth)/update-password/page.tsx
import { UpdatePasswordForm } from '@/features/auth/components'
export default function UpdatePasswordPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="w-full max-w-md space-y-8 p-8">
<div className="text-center">
<h1 className="text-3xl font-bold">Set new password</h1>
<p className="mt-2 text-gray-600">Enter your new password below</p>
</div>
<UpdatePasswordForm />
</div>
</div>
)
}
16. src/app/(auth)/callback/route.ts
import { NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url)
const code = searchParams.get('code')
const next = searchParams.get('next') ?? '/dashboard'
if (code) {
const supabase = await createClient()
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
return NextResponse.redirect(`${origin}${next}`)
}
}
return NextResponse.redirect(`${origin}/login?error=auth_callback_failed`)
}
17. src/features/auth/components/GoogleSignInButton.tsx
'use client'
import { useState } from 'react'
import { createClient } from '@/lib/supabase/client'
interface GoogleSignInButtonProps {
redirectTo?: string
label?: string
}
export function GoogleSignInButton({
redirectTo = '/dashboard',
label = 'Continuar con Google',
}: GoogleSignInButtonProps) {
const [loading, setLoading] = useState(false)
async function handleGoogleSignIn() {
setLoading(true)
const supabase = createClient()
const { error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/callback?next=${redirectTo}`,
queryParams: {
access_type: 'offline',
prompt: 'consent',
},
},
})
if (error) {
console.error('Google sign-in error:', error.message)
setLoading(false)
}
}
return (
<button
type="button"
onClick={handleGoogleSignIn}
disabled={loading}
className="flex w-full items-center justify-center gap-3 rounded-md border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 shadow-sm transition-colors hover:bg-gray-50 disabled:opacity-50"
>
<svg className="h-5 w-5" viewBox="0 0 24 24">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4" />
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853" />
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05" />
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335" />
</svg>
{loading ? 'Redirigiendo...' : label}
</button>
)
}
18. src/features/auth/components/AuthDivider.tsx
export function AuthDivider() {
return (
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center text-sm">
<span className="bg-white px-2 text-gray-500">o</span>
</div>
</div>
)
}
Flujo de Ejecucion
- Crear TODOS los archivos de codigo listados arriba
- Verificar que
@supabase/ssr este instalado (si no: npm install @supabase/ssr)
- Usar Supabase MCP para crear la tabla profiles:
Usa el MCP de Supabase con `apply_migration` para ejecutar:
-- Tabla profiles
create table public.profiles (
id uuid references auth.users on delete cascade primary key,
email text not null,
full_name text,
avatar_url text,
created_at timestamptz default now() not null,
updated_at timestamptz default now() not null
);
-- RLS
alter table public.profiles enable row level security;
create policy "Users can view own profile"
on public.profiles for select
using (auth.uid() = id);
create policy "Users can update own profile"
on public.profiles for update
using (auth.uid() = id);
-- Trigger: crear perfil automaticamente al signup
create or replace function public.handle_new_user()
returns trigger as $$
begin
insert into public.profiles (id, email, full_name, avatar_url)
values (
new.id,
new.email,
coalesce(new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'name'),
new.raw_user_meta_data->>'avatar_url'
);
return new;
end;
$$ language plpgsql security definer;
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();
- Mostrar mensaje de completacion
Mensaje Final
Despues de crear archivos Y ejecutar la migracion, muestra:
Auth B2B implementado!
Incluye:
- Login/Signup con Email/Password
- Login/Signup con Google OAuth
- Password Reset completo
- Tabla profiles (creada vía MCP) con full_name y avatar_url de Google
- Hook useAuth() con user + profile
- Rutas protegidas (/dashboard)
- Callback OAuth (/callback)
- Action updateProfile() para editar perfil
Configurar credenciales:
1. Ve a supabase.com > tu proyecto > Settings > API
2. Copia a .env.local:
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbG...
NEXT_PUBLIC_SITE_URL=http://localhost:3000
3. En Authentication > URL Configuration:
- Site URL: http://localhost:3000
- Redirect URLs: http://localhost:3000/**
4. Para Google OAuth:
a. Google Cloud Console > APIs & Services > Credentials
b. Crear OAuth 2.0 Client ID (tipo: Web application)
c. Authorized redirect URI: https://TU_PROJECT_REF.supabase.co/auth/v1/callback
d. En Supabase Dashboard > Authentication > Providers > Google:
- Habilitar Google provider
- Pegar Client ID y Client Secret de Google
5. npm run dev
Listo para probar en /login (Email/Password + Google)