Saltar al contenido
Volver a proyectos
Cover
Proyecto personal

Admin Dashboard — Next.js 16 + Supabase

Admin Dashboard — Next.js 16 + Supabase

Una ruta /admin segura construida con HTTP Basic Auth, Supabase service_role, y cero dependencias adicionales.

Por qué lo construí

Todo portfolio basado en datos acaba necesitando una forma de supervisar sus propios datos sin abrir el dashboard de Supabase cada vez. Quería ver mis mensajes de contacto, el número de proyectos y el estado de las certificaciones de un vistazo — pero construir un sistema de autenticación completo parecía una sobreingeniería enorme para una herramienta personal.

El reto: ¿cómo proteger una ruta interna en Next.js 16 sin añadir ninguna dependencia nueva, manteniendo la capacidad de leer tablas bloqueadas por Row Level Security para usuarios anónimos?

Restricciones técnicas que tuve que resolver

  • Next.js 16 sustituye middleware.ts por proxy.ts — el mismo archivo gestiona el enrutamiento i18n de next-intl. La autenticación admin debía inyectarse sin romper los prefijos de idioma.
  • El entorno Edge no tiene Buffer: el enfoque estándar de Node.js (Buffer.from(b64, 'base64').toString()) falla silenciosamente. Tuve que usar atob() en su lugar.
  • La tabla messages está bloqueada para SELECT por política RLS en el rol anon. Leerla requiere un cliente service_role — que nunca debe salir del servidor.
  • El layout admin NO debe incluir <html>/<body> — el app/layout.tsx raíz ya los proporciona. Duplicarlos provoca un desajuste de hidratación en React.
  • La ruta /admin debe excluirse de la lógica de prefijado de idioma de next-intl, o el proxy intentará redirigir /admin a /en/admin.

Cómo lo construí

  1. 1
    Paso 1 — Cliente Supabase admin
    Creé lib/supabase/admin.ts usando la variable de entorno SUPABASE_SERVICE_ROLE_KEY (sin prefijo NEXT_PUBLIC_ — solo servidor). Este cliente omite todas las políticas RLS y puede hacer SELECT sobre la tabla messages a la que el cliente anon público no tiene acceso.
  2. 2
    Paso 2 — HTTP Basic Auth en proxy.ts
    Añadí un guard adminAuth() al principio de la función proxy(). Si la ruta empieza por /admin, comprueba la cabecera Authorization antes de pasar al handler de next-intl. Usé atob() para una decodificación base64 compatible con Edge. ADMIN_USERNAME y ADMIN_PASSWORD se definen en .env.local (solo servidor).
  3. 3
    Paso 3 — Layout admin aislado
    Creé app/admin/layout.tsx devolviendo un <div> envolvente (no <html>/<body>) con un tema oscuro Slate, una cabecera mínima, y metadata robots: index: false. El layout está completamente fuera del árbol de enrutamiento [locale].
  4. 4
    Paso 4 — Server Component del dashboard
    app/admin/page.tsx obtiene las cuatro tablas en paralelo con Promise.all(). Tarjetas de estadísticas, una tabla completa de proyectos (estado / área / destacado / fecha), los 20 mensajes de contacto más recientes con enlaces mailto, una tabla de certificaciones, y contadores de testimonios. Cero JavaScript de cliente.

Núcleo: Basic Auth compatible con Edge (proxy.ts)

function adminAuth(request: NextRequest): NextResponse | null {
  const expectedUser = process.env.ADMIN_USERNAME;
  const expectedPass = process.env.ADMIN_PASSWORD;

  if (!expectedUser || !expectedPass) {
    return new NextResponse("Admin not configured.", { status: 503 });
  }

  const authHeader = request.headers.get("authorization");
  if (authHeader?.startsWith("Basic ")) {
    try {
      const decoded = atob(authHeader.slice(6)); // Edge-safe — no Buffer
      const colonIdx = decoded.indexOf(":");
      if (colonIdx !== -1) {
        const user = decoded.slice(0, colonIdx);
        const pass = decoded.slice(colonIdx + 1);
        if (user === expectedUser && pass === expectedPass) {
          return null; // ✅ authorized
        }
      }
    } catch {
      // malformed base64 — reject
    }
  }

  return new NextResponse("Authentication required.", {
    status: 401,
    headers: { "WWW-Authenticate": 'Basic realm="Admin", charset="UTF-8"' },
  });
}

Qué demuestra este proyecto

0
Paquetes npm nuevos añadidos
HTTP Basic Auth vía proxy.ts + variables de entorno únicamente
4
Tablas monitorizadas
projects · certifications · messages · testimonials
0 KB
JavaScript de cliente
100% Server Components — sin sobrecarga de hidratación
~25
Líneas de código de autenticación
En proxy.ts — sin página de login, sin sesión, sin JWT

Aprendizajes clave

  • El entorno Edge es más restrictivo que Node.js: siempre hay que comprobar qué APIs están disponibles (Buffer, crypto, fs…) antes de usarlas.
  • next-intl y una lógica de proxy personalizada pueden convivir sin problemas si proteges la ruta admin antes de llamar al handler de intl.
  • Los Server Components son la opción correcta por defecto para dashboards internos — sin estado, sin efectos, sin bundles enviados al navegador.
  • RLS es una red de seguridad, no un sustituto de los secretos del lado servidor: incluso con service_role, la clave nunca sale del servidor.