Sistema de Descarga Segura de Productos

Implementación de un sistema robusto para permitir la descarga segura de archivos adquiridos, utilizando Next.js, Supabase Storage, y verificaciones de autorización.

13 min de lectura
Por Equipo 10xDev

Objetivo: Implementar un sistema robusto y seguro para permitir a los usuarios que han comprado un paquete descargar los archivos correspondientes (.zip).

Flujo General:

  1. El paquete de boilerplate/template se comprime en un archivo .zip.
  2. El archivo .zip se sube a un bucket privado en Supabase Storage.
  3. El usuario compra el producto a través de Stripe Checkout.
  4. El webhook de Stripe registra la compra exitosa en nuestra DB (user_products).
  5. El usuario navega a la página de "Descargas" en su dashboard (requiere autenticación).
  6. La página de descargas lista los productos comprados por el usuario.
  7. El usuario hace clic en el botón de descarga para un producto específico.
  8. Esto desencadena una llamada a una API Route segura en nuestro backend.
  9. La API Route verifica:
    • Que el usuario esté autenticado.
    • Que el usuario haya comprado el producto solicitado (consultando user_products).
  10. Si está autorizado, la API Route streamea el archivo .zip desde Supabase Storage directamente al navegador del usuario.

Prerrequisitos:

  • Proyecto Next.js con Pages Router, TypeScript, Tailwind, pnpm configurado.
  • Supabase integrado (Auth, Database, Storage).
  • Stripe integrado (Checkout, Webhooks configurados y funcionales, registrando compras en user_products - esta parte se asume ya implementada del flujo de pago).
  • Resend integrado (usado para notificaciones, no directamente en el flujo de descarga per se, pero parte del ecosistema).
  • Acceso a las credenciales de Supabase (URL, anon key, service_role key) y Stripe (Secret Key, Webhook Secret).

Paso 1: Preparación del Archivo Descargable (.zip)

Cada paquete de producto (10xDev Advanced, 10xDev Full) debe ser un único archivo comprimido.

1 Comprimir los Archivos: Asegúrate de que todo el contenido del boilerplate o template esté dentro de un archivo .zip. 2 Nombrar el Archivo: Usa un nombre de archivo consistente y que puedas mapear fácilmente desde tu código. Sugerimos usar el identificador interno del producto.

  • Para 10xDev Full: 10xdev-full.zip
  • Para 10xDev Advanced: 10xdev-advanced.zip

Paso 2: Configuración del Bucket de Supabase Storage#

Los archivos descargables deben estar en un lugar seguro y no accesible públicamente. Supabase Storage con políticas adecuadas es ideal.

1 Crear un Nuevo Bucket:

  • Ve al dashboard de Supabase.
  • Navega a "Storage".
  • Haz clic en "New bucket".
  • Nombra el bucket (ej: product-downloads).
  • MUY IMPORTANTE: Desmarca la opción "Public bucket". El bucket debe ser privado.
  • Haz clic en "Create bucket".

2 Configurar Políticas de Acceso (Policies):

  • Una vez creado el bucket, selecciónalo.
  • Ve a la pestaña "Policies".
  • Por defecto, un bucket privado niega el acceso anon. Sin embargo, es buena práctica ser explícito.
  • Asegúrate de que NO haya políticas que permitan SELECT para roles anon o authenticated.
  • La forma más segura para nuestro caso (donde una API route gestiona el acceso) es que la API route use la clave service_role de Supabase (que bypassa RLS) o genere URLs firmadas temporalmente después de verificar el acceso en código. Si usamos la clave service_role en la API, no necesitamos RLS complejas en el bucket; la política por defecto que niega el acceso público/autenticado es suficiente.
  • Verifica que no haya políticas que permitan SELECT a usuarios no autorizados. La política por defecto para buckets privados suele ser suficiente, pero siempre revisa. El acceso lo gestionará tu backend.

3 Subir los Archivos .zip:

  • Dentro del bucket product-downloads, sube los archivos .zip que creaste en el Paso 1 (ej: 10xdev-full.zip, 10xdev-advanced.zip). Puedes subirlos directamente a la raíz del bucket o crear una estructura de carpetas si lo prefieres (ej: downloads/10xdev-full.zip), pero recuerda la ruta.

Paso 3: Verificación del Modelo de Datos user_products

Asegúrate de tener la tabla que registra qué usuario compró qué producto.

  • Tabla: user_products

    • id: uuid (PK)
    • user_id: uuid (FK a auth.users, NOT NULL) - CRÍTICO para saber quién compró.
    • product_id: text (Identificador interno del producto, ej: '10xdev-full', '10xdev-advanced', NOT NULL) - Este ID debe coincidir con el que usarás en tu código y en el nombre del archivo .zip.
    • stripe_checkout_session_id: text (Vinculación con Stripe, NOT NULL)
    • purchased_at: timestamp with time zone (NOT NULL, default now())
    • created_at: timestamp with time zone (NOT NULL, default now())
  • RLS en user_products: Configura políticas de RLS para user_products que solo permitan a un usuario SELECT sus propios registros (auth.uid() = user_id). Esto protege los datos del usuario si alguna vez haces consultas directas desde el frontend (aunque nuestra API lo verificará también).


Paso 4: Implementación de la API Route de Descarga Segura#

Esta es la pieza central de seguridad. Controla quién puede descargar qué.

Archivo: pages/api/download/[productId].ts

Explicación del Código de la API Route:

  • Importaciones: Importa lo necesario de Next.js, Supabase, y tus helpers.
  • Clientes Supabase: Se inicializa un cliente supabaseServiceRole usando la clave SUPABASE_SERVICE_ROLE_KEY. Esta clave tiene permisos elevados y bypassa las políticas de RLS en la base de datos y storage, permitiéndonos leer archivos privados después de haber verificado la autorización del usuario con el cliente estándar.
  • productToFileMap: Un objeto simple que mapea el product_id interno (usado en tu DB y código) a la ruta del archivo en el bucket de Supabase Storage. Mantén esto actualizado.
  • handler: La función principal de la API Route.
  • Verificación de Método HTTP: Solo acepta peticiones GET.
  • Extracción productId: Obtiene el ID del producto de la URL.
  • Verificación de Autenticación (getUser): Usa tu helper para obtener el usuario logueado. Si no hay usuario, devuelve 401.
  • Verificación de Autorización (supabase.from('user_products').select()...): Esta es la verificación de negocio. Consulta la tabla user_products para confirmar que existe una entrada para el user.id autenticado y el productId solicitado. Si no se encuentra, devuelve 403.
  • Mapeo a Ruta de Archivo: Usa productToFileMap para encontrar la ruta del archivo .zip correspondiente en Supabase Storage.
  • Descarga del Archivo (supabaseServiceRole.storage.from().download()): Usa el cliente con service_role para descargar el contenido del archivo del bucket privado.
  • Configuración de Cabeceras: Establece el Content-Type a application/zip (o el correcto) y Content-Disposition para forzar al navegador a descargar el archivo y darle un nombre.
  • Envío del Archivo: Convierte el Blob recibido de Supabase a Buffer y lo envía como respuesta.
  • Manejo de Errores: Captura errores en cada etapa (DB, Storage, inesperados) y responde con códigos de estado apropiados (500, 404, etc.).

Notas de Seguridad:

  • SUPABASE_SERVICE_ROLE_KEY: Esta clave es muy poderosa. DEBE almacenarse como una variable de entorno SECRETA en tu entorno de producción y NUNCA debe ser expuesta al código del lado del cliente.
  • Verificación Doble: La seguridad reside en la doble verificación: autenticación del usuario y la consulta a user_products para verificar la compra antes de intentar acceder al archivo.
  • No Exponer Rutas Directas: La API Route es el único punto de acceso a los archivos privados. Nunca enlaces directamente a archivos en Supabase Storage desde el frontend a menos que uses URLs firmadas generadas en el backend.

Paso 5: Implementación del Frontend (Página de Descargas del Dashboard)#

Esta página muestra al usuario lo que ha comprado y cómo descargarlo.

Archivo: pages/dashboard/downloads.tsx

Explicación del Código Frontend:

  • getServerSideProps: Se ejecuta en el servidor antes de renderizar la página.
    • Verifica la autenticación del usuario usando getUser. Si no hay usuario, redirige al login.
    • Consulta la tabla user_products para obtener los product_id asociados al usuario logueado.
    • Pasa los datos del usuario y los productos comprados como props al componente de la página.
  • DownloadsPage Componente:
    • Recibe user y purchasedProducts como props.
    • Muestra un título y una lista de los productos comprados.
    • Para cada producto, crea un <a> tag usando next/link.
    • El href del enlace apunta a nuestra API Route de descarga segura: /api/download/${item.product_id}. Cuando el usuario hace clic en este enlace, el navegador hará una petición GET a esa URL, activando nuestra lógica de backend.
    • Se incluye un mapeo simple productDisplayName para mostrar nombres de productos más amigables en la UI.

Paso 6: Variables de Entorno#

Asegúrate de tener configuradas las variables de entorno necesarias.

  • .env.local (para desarrollo) y variables de entorno en tu proveedor de hosting (Vercel, Netlify, etc.) para producción.
  • NEXT_PUBLIC_SUPABASE_URL: URL de tu proyecto Supabase (accesible públicamente).
  • NEXT_PUBLIC_SUPABASE_ANON_KEY: Clave anon pública de Supabase (accesible públicamente).
  • SUPABASE_SERVICE_ROLE_KEY: La clave service_role de Supabase. SECRETA Solo debe estar en el lado del servidor.
  • STRIPE_SECRET_KEY: Tu clave secreta de Stripe (SECRETA).
  • STRIPE_WEBHOOK_SECRET: El secreto de firma de tu webhook de Stripe (SECRETA).

Prueba y Despliegue#

1 Desarrollo: Ejecuta tu proyecto localmente (pnpm dev). Sube un archivo .zip a tu bucket privado de Supabase Storage. Edita manualmente tu tabla user_products en Supabase para simular una compra (vinculando tu user_id a un product_id que tenga un archivo .zip asociado). Navega a /dashboard/downloads (asegúrate de estar logueado) y prueba la descarga. 2 Producción: Despliega tu código. Asegúrate de que las variables de entorno secretas estén configuradas correctamente en tu entorno de hosting. Realiza una compra real a través de tu flujo de Stripe para asegurarte de que el webhook registre la compra y puedas descargar el producto.