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.
Objetivo: Implementar un sistema robusto y seguro para permitir a los
usuarios que han comprado un paquete descargar los archivos correspondientes
(.zip
).
Flujo General:
- El paquete de boilerplate/template se comprime en un archivo
.zip
. - El archivo
.zip
se sube a un bucket privado en Supabase Storage. - El usuario compra el producto a través de Stripe Checkout.
- El webhook de Stripe registra la compra exitosa en nuestra DB
(
user_products
). - El usuario navega a la página de "Descargas" en su dashboard (requiere autenticación).
- La página de descargas lista los productos comprados por el usuario.
- El usuario hace clic en el botón de descarga para un producto específico.
- Esto desencadena una llamada a una API Route segura en nuestro backend.
- La API Route verifica:
- Que el usuario esté autenticado.
- Que el usuario haya comprado el producto solicitado (consultando
user_products
).
- 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 rolesanon
oauthenticated
. - 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 claveservice_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 aauth.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, defaultnow()
)created_at
:timestamp with time zone
(NOT NULL, defaultnow()
)
-
RLS en
user_products
: Configura políticas de RLS parauser_products
que solo permitan a un usuarioSELECT
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 claveSUPABASE_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 elproduct_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 tablauser_products
para confirmar que existe una entrada para eluser.id
autenticado y elproductId
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 conservice_role
para descargar el contenido del archivo del bucket privado. - Configuración de Cabeceras: Establece el
Content-Type
aapplication/zip
(o el correcto) yContent-Disposition
para forzar al navegador a descargar el archivo y darle un nombre. - Envío del Archivo: Convierte el
Blob
recibido de Supabase aBuffer
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 losproduct_id
asociados al usuario logueado. - Pasa los datos del usuario y los productos comprados como
props
al componente de la página.
- Verifica la autenticación del usuario usando
DownloadsPage
Componente:- Recibe
user
ypurchasedProducts
como props. - Muestra un título y una lista de los productos comprados.
- Para cada producto, crea un
<a>
tag usandonext/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ónGET
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.
- Recibe
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
: Claveanon
pública de Supabase (accesible públicamente).SUPABASE_SERVICE_ROLE_KEY
: La claveservice_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.