🔐 Clerk Authentication
Visión General
PuntoHack utiliza Clerk como servicio de autenticación, proporcionando autenticación completa sin necesidad de gestionar usuarios, passwords, o sesiones manualmente.
Configuración
Paso 1: Crear Aplicación en Clerk
- Ve a Clerk Dashboard
- Crea una nueva aplicación
- Copia las siguientes claves:
- Publishable Key (ej:
pk_test_...) - Secret Key (ej:
sk_test_...)
- Publishable Key (ej:
Paso 2: Variables de Entorno
Agrega a .env.local:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard
Paso 3: Middleware
Archivo: middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isPublicRoute = createRouteMatcher([
'/',
'/sign-in(.*)',
'/sign-up(.*)',
'/hackathons(.*)',
'/api/webhooks(.*)',
]);
export default clerkMiddleware(async (auth, req) => {
if (!isPublicRoute(req)) {
await auth.protect();
}
});
export const config = {
matcher: [
'/((?!_next|[^?]*\\.(?:html?|ico|svg)).*)',
'/(api|trpc)(.*)',
],
};
Uso en Server Actions
Obtener Usuario Actual
Archivo: src/core/auth.ts
import { auth, currentUser } from '@clerk/nextjs/server';
import { db } from './db';
export async function getCurrentUser() {
const { userId } = await auth();
if (!userId) return null;
const profile = await db.profile.findUnique({
where: { userId },
});
if (!profile) return null;
return {
id: userId,
profile,
};
}
export async function getClerkUserId(): Promise<string | null> {
const { userId } = await auth();
return userId;
}
Verificar Autenticación
import { getCurrentUser } from '@/core/auth';
export async function myServerAction() {
const user = await getCurrentUser();
if (!user) {
throw new Error('Unauthorized');
}
// Continuar con lógica...
}
Sincronización con Profile
Webhook de Clerk
Endpoint: /api/webhooks/clerk
Eventos:
user.created- Crear profile automáticamenteuser.updated- Actualizar profileuser.deleted- Eliminar profile (opcional)
Implementación:
import { Webhook } from 'svix';
import { headers } from 'next/headers';
import { db } from '@/core/db';
export async function POST(req: Request) {
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
if (!WEBHOOK_SECRET) {
throw new Error('WEBHOOK_SECRET not set');
}
const headerPayload = await headers();
const svix_id = headerPayload.get('svix-id');
const svix_timestamp = headerPayload.get('svix-timestamp');
const svix_signature = headerPayload.get('svix-signature');
if (!svix_id || !svix_timestamp || !svix_signature) {
return new Response('Error occurred', { status: 400 });
}
const payload = await req.json();
const body = JSON.stringify(payload);
const wh = new Webhook(WEBHOOK_SECRET);
let evt: WebhookEvent;
try {
evt = wh.verify(body, {
'svix-id': svix_id,
'svix-timestamp': svix_timestamp,
'svix-signature': svix_signature,
}) as WebhookEvent;
} catch (err) {
return new Response('Error occurred', { status: 400 });
}
const eventType = evt.type;
if (eventType === 'user.created') {
const { id, email_addresses, first_name, last_name } = evt.data;
await db.profile.create({
data: {
userId: id,
email: email_addresses[0].email_address,
name: `${first_name || ''} ${last_name || ''}`.trim() || 'User',
role: 'PARTICIPANT',
},
});
}
return new Response('', { status: 200 });
}
Componentes de Autenticación Personalizados
PuntoHack implementa componentes de autenticación personalizados usando los hooks de Clerk para tener control total sobre el diseño y la experiencia de usuario.
Custom Sign-In Component
Archivo: src/components/auth/custom-sign-in.tsx
'use client';
import { useSignIn } from '@clerk/nextjs';
import { useState } from 'react';
export function CustomSignIn() {
const { isLoaded, signIn, setActive } = useSignIn();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!isLoaded) return;
setLoading(true);
setError(null);
try {
const result = await signIn.create({
identifier: email,
password,
});
if (result.status === 'complete') {
await setActive({ session: result.createdSessionId });
window.location.href = '/dashboard';
}
} catch (err: any) {
setError(err.errors?.[0]?.message || 'Error al iniciar sesión');
} finally {
setLoading(false);
}
};
const handleOAuthSignIn = async (strategy: 'oauth_google' | 'oauth_github') => {
if (!isLoaded) return;
try {
await signIn.authenticateWithRedirect({
strategy,
redirectUrl: `${window.location.origin}/sso-callback`,
redirectUrlComplete: `${window.location.origin}/dashboard`,
});
} catch (err: any) {
setError(err.errors?.[0]?.message || 'Error al iniciar sesión con OAuth');
}
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Botones OAuth */}
<div className="space-y-3">
<button
type="button"
onClick={() => handleOAuthSignIn('oauth_google')}
className="w-full flex items-center justify-center gap-2 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
>
<span>Continuar con Google</span>
</button>
<button
type="button"
onClick={() => handleOAuthSignIn('oauth_github')}
className="w-full flex items-center justify-center gap-2 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
>
<span>Continuar con GitHub</span>
</button>
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-white px-2 text-gray-500">O</span>
</div>
{/* Formulario Email/Password */}
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="password">Contraseña</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
{error && <div className="text-red-600 text-sm">{error}</div>}
<button type="submit" disabled={loading || !isLoaded}>
{loading ? 'Cargando...' : 'Iniciar Sesión'}
</button>
{/* Elemento oculto para CAPTCHA de Clerk */}
<div id="clerk-captcha" className="hidden"></div>
</form>
);
}
Custom Sign-Up Component
Similar al Sign-In, pero usando useSignUp hook:
Archivo: src/components/auth/custom-sign-up.tsx
'use client';
import { useSignUp } from '@clerk/nextjs';
import { useState } from 'react';
export function CustomSignUp() {
const { isLoaded, signUp, setActive } = useSignUp();
// ... similar estructura al Sign-In
}
Páginas de Autenticación
Sign-In Page: src/app/sign-in/[[...sign-in]]/page.tsx
import { CustomSignIn } from '@/components/auth/custom-sign-in';
export default function SignInPage() {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50">
<div className="w-full max-w-md p-8 bg-white rounded-lg shadow-xl">
<h1 className="text-2xl font-bold text-center mb-6">Iniciar Sesión</h1>
<CustomSignIn />
</div>
</div>
);
}
SSO Callback Page
Archivo: src/app/sso-callback/page.tsx
Página intermedia que maneja el redirect después de autenticación OAuth:
'use client';
import { useAuth } from '@clerk/nextjs';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
export default function SSOCallbackPage() {
const { isLoaded, isSignedIn } = useAuth();
const router = useRouter();
useEffect(() => {
if (isLoaded) {
if (isSignedIn) {
router.push('/dashboard');
} else {
setTimeout(() => {
router.push('/sign-in');
}, 2000);
}
}
}, [isLoaded, isSignedIn, router]);
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<p>Completando autenticación...</p>
</div>
</div>
);
}
Middleware Update
El middleware debe permitir la ruta /sso-callback:
const isPublicRoute = createRouteMatcher([
'/',
'/sign-in(.*)',
'/sign-up(.*)',
'/sso-callback(.*)', // ← Agregar esta línea
'/hackathons(.*)',
]);
OAuth Providers
PuntoHack soporta autenticación OAuth con:
- Google (
oauth_google) - GitHub (
oauth_github)
Configuración en Clerk
- Ve a Clerk Dashboard
- User & Authentication → Social Connections
- Habilita Google y/o GitHub
- Configura las credenciales OAuth de cada proveedor
Uso en Componentes
Client Components
'use client';
import { useUser } from '@clerk/nextjs';
export default function MyComponent() {
const { user, isLoaded } = useUser();
if (!isLoaded) {
return <div>Loading...</div>;
}
if (!user) {
return <div>Not authenticated</div>;
}
return <div>Hello {user.firstName}</div>;
}
Server Components
import { currentUser } from '@clerk/nextjs/server';
export default async function MyServerComponent() {
const user = await currentUser();
if (!user) {
return <div>Not authenticated</div>;
}
return <div>Hello {user.firstName}</div>;
}
Rutas Protegidas
Con Middleware
// middleware.ts
const isProtectedRoute = createRouteMatcher([
'/dashboard(.*)',
'/organizer(.*)',
'/judge(.*)',
'/sponsor(.*)',
'/admin(.*)',
]);
export default clerkMiddleware(async (auth, req) => {
if (isProtectedRoute(req)) {
await auth.protect();
}
});
En Server Actions
export async function protectedAction() {
const user = await getCurrentUser();
if (!user) {
throw new Error('Unauthorized');
}
// Continuar...
}
Próximos Pasos
- Supabase Realtime - Integración con Supabase
- Prisma ORM - Uso de Prisma
- Development Guide - Setup completo
Siguiente: Prisma ORM