Saltar al contenido principal

🔐 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

  1. Ve a Clerk Dashboard
  2. Crea una nueva aplicación
  3. Copia las siguientes claves:
    • Publishable Key (ej: pk_test_...)
    • Secret Key (ej: sk_test_...)

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áticamente
  • user.updated - Actualizar profile
  • user.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

  1. Ve a Clerk Dashboard
  2. User & AuthenticationSocial Connections
  3. Habilita Google y/o GitHub
  4. 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


Siguiente: Prisma ORM