Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# ---------------------------------------------------------------------------
# App
# ---------------------------------------------------------------------------
NEXT_PUBLIC_APP_URL=http://localhost:3000

# ---------------------------------------------------------------------------
# Supabase (Auth + Storage)
# ---------------------------------------------------------------------------
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=

# ---------------------------------------------------------------------------
# Base de datos (Supabase Postgres vía Prisma)
# ---------------------------------------------------------------------------
DATABASE_URL=
DIRECT_URL=

# ---------------------------------------------------------------------------
# Stripe
# ---------------------------------------------------------------------------
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=

# ---------------------------------------------------------------------------
# MercadoPago
# ---------------------------------------------------------------------------
MERCADOPAGO_ACCESS_TOKEN=
MERCADOPAGO_WEBHOOK_SECRET=
NEXT_PUBLIC_MERCADOPAGO_PUBLIC_KEY=

# ---------------------------------------------------------------------------
# OpenAI (generación de contenido LinkedIn, RAG, documentación asistida, etc.)
# ---------------------------------------------------------------------------
OPENAI_API_KEY=

# ---------------------------------------------------------------------------
# LinkedIn API v2
# ---------------------------------------------------------------------------
LINKEDIN_CLIENT_ID=
LINKEDIN_CLIENT_SECRET=
LINKEDIN_ACCESS_TOKEN=
LINKEDIN_ORGANIZATION_ID=

# ---------------------------------------------------------------------------
# Seguridad
# ---------------------------------------------------------------------------
# Clave de 32 bytes en base64 para cifrar credenciales de integraciones (AES-256-GCM).
# Generar con: openssl rand -base64 32
ENCRYPTION_KEY=
# Secreto compartido para proteger los endpoints /api/cron/*.
CRON_SECRET=

# ---------------------------------------------------------------------------
# Seed (opcional, valores por defecto en prisma/seed.ts)
# ---------------------------------------------------------------------------
SEED_ADMIN_EMAIL=admin@nexusops.dev
SEED_ADMIN_PASSWORD=AdminDev123!
SEED_DEMO_EMAIL=demo@nexusops.dev
SEED_DEMO_PASSWORD=DemoClient123!
7 changes: 7 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": ["next/core-web-vitals", "next/typescript"],
"rules": {
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
"@typescript-eslint/no-explicit-any": "warn"
}
}
42 changes: 42 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*
!.env.example

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
259 changes: 259 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
# NexusOps — Plataforma SaaS B2B multiservicio de tecnología e IA

Plataforma multi-tenant que comercializa, gestiona y entrega 20 servicios tecnológicos y de inteligencia artificial
para empresas (automatización, CTO fraccional, chatbots RAG, cumplimiento de datos, formación, observabilidad de
LLM, etc.), cada uno con su propia página pública, planes, flujo de contratación, panel operativo y facturación.

## Stack

- **Frontend/Backend**: Next.js 14 (App Router), TypeScript estricto, Tailwind CSS, shadcn/ui-style components
- **Base de datos**: PostgreSQL vía Supabase, Prisma ORM
- **Auth**: Supabase Auth (email/password, Google OAuth, recuperación de contraseña)
- **Storage**: Supabase Storage (archivos con URLs firmadas)
- **Pagos**: Stripe + MercadoPago
- **IA**: OpenAI API (generación de contenido, RAG, documentación)
- **Automatización**: LinkedIn API v2 vía adaptador propio + Vercel Cron
- **Tests**: Vitest (unit/integration) + Playwright (E2E)

## 1. Requisitos

- Node.js 20+
- npm 10+
- Una cuenta de Supabase (proyecto Postgres + Auth + Storage)
- Cuenta de Stripe (modo test) y, opcionalmente, MercadoPago
- Stripe CLI (para probar webhooks en local)
- Cuenta de OpenAI (opcional, requerida para RAG/documentación asistida/LinkedIn)
- App de LinkedIn Developer (opcional, requerida solo para publicar de verdad)

## 2. Instalación

```bash
npm install
cp .env.example .env
```

Completa `.env` con tus credenciales (ver secciones siguientes). La aplicación **arranca sin errores** aunque
falten las claves de terceros (Stripe/MercadoPago/OpenAI/LinkedIn); esas integraciones fallan con un error claro
solo al intentar usarlas, nunca de forma silenciosa ni simulada.

## 3. Configuración de Supabase

1. Crea un proyecto en [supabase.com](https://supabase.com).
2. En **Project Settings → API**, copia:
- `NEXT_PUBLIC_SUPABASE_URL`
- `NEXT_PUBLIC_SUPABASE_ANON_KEY`
- `SUPABASE_SERVICE_ROLE_KEY` (¡nunca exponer al cliente!)
3. En **Project Settings → Database**, copia la cadena de conexión (modo *Transaction* para `DATABASE_URL` con
pooler, y modo *Session*/directa para `DIRECT_URL`, usada por las migraciones de Prisma).
4. En **Authentication → Providers**, habilita **Email** y **Google** (ver sección 6).
5. En **Authentication → URL Configuration**, agrega como Redirect URL:
`http://localhost:3000/auth/callback` (y el equivalente de producción).
6. En **Storage**, crea un bucket llamado `service-files` (privado). El backend sube y lee archivos usando la
Service Role Key con URLs firmadas de corta duración — nunca lo expongas como bucket público.
7. (Opcional, defensa en profundidad) Aplica las políticas RLS incluidas:
```bash
psql "$DATABASE_URL" -f prisma/rls-policies.sql
```
La autorización real ocurre siempre en el servidor (`lib/auth/guards.ts`); RLS es una capa adicional.

## 4. Configuración de Prisma y migraciones

```bash
npx prisma generate
npx prisma migrate dev --name init
```

Esto crea todas las tablas del `prisma/schema.prisma` (Organization, User, Membership, Service, Plan, Subscription,
Usage, ServiceRequest, ServiceExecution, Deliverable, UploadedFile, SupportTicket, TicketMessage, Invoice, Payment,
OrganizationIntegration, Notification, AuditLog, WebhookEvent, CreditLedger, OnboardingProgress,
ServiceConfiguration, ServiceFormDefinition, ServiceFormSubmission, Lead, BlogPost, LinkedInQueue).

Para producción:

```bash
npx prisma migrate deploy
```

## 5. Seed (datos iniciales)

```bash
npm run db:seed
```

Carga:

- Los 20 servicios del catálogo (`lib/services/catalog.ts`) con sus 3 planes cada uno (Starter/Pro/Enterprise)
- El `ServiceFormDefinition` de cada servicio (a partir de `lib/services/workflows.ts`)
- 5 artículos de blog
- Un usuario **administrador** (`SEED_ADMIN_EMAIL` / `SEED_ADMIN_PASSWORD`, por defecto
`admin@nexusops.dev` / `AdminDev123!`)
- Un usuario y organización de **demostración** (`SEED_DEMO_EMAIL` / `SEED_DEMO_PASSWORD`) con una suscripción,
consumo y créditos de ejemplo

El seed se niega a ejecutarse con `NODE_ENV=production` a menos que definas `ALLOW_PROD_SEED=true` — **no está
pensado para cargar datos de demostración en producción**.

## 6. Configuración de Google OAuth

1. En [Google Cloud Console](https://console.cloud.google.com/), crea credenciales OAuth 2.0 (tipo *Web application*).
2. Authorized redirect URI: la URL de callback de tu proyecto Supabase
(`https://<project-ref>.supabase.co/auth/v1/callback`).
3. Copia Client ID/Secret a **Supabase → Authentication → Providers → Google**.

## 7. Configuración de Stripe

1. Copia `STRIPE_SECRET_KEY` y `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` desde el dashboard de Stripe (modo test).
2. En cada `Plan` (desde `/admin/servicios/[id]`), completa `stripePriceIdMonthly` / `stripePriceIdAnnual` con los
Price IDs reales creados en Stripe (Product + Price). Sin esto, el checkout de ese plan devuelve un error claro
en vez de simular un pago.
3. Escucha webhooks en local con Stripe CLI:
```bash
stripe listen --forward-to localhost:3000/api/webhooks/stripe
```
Copia el `whsec_...` que imprime a `STRIPE_WEBHOOK_SECRET`.
4. Eventos procesados: `checkout.session.completed`, `customer.subscription.created`,
`customer.subscription.updated`, `customer.subscription.deleted`, `invoice.paid`, `invoice.payment_failed`,
`charge.refunded`. Todos son idempotentes vía la tabla `WebhookEvent`.
5. El Customer Portal de Stripe se usa para cambios de plan/cancelación desde `/dashboard/facturacion`.

## 8. Configuración de MercadoPago

1. Copia `MERCADOPAGO_ACCESS_TOKEN` (modo test) y `NEXT_PUBLIC_MERCADOPAGO_PUBLIC_KEY`.
2. Configura la URL de webhooks en el panel de MercadoPago (**Tus integraciones → Webhooks**) apuntando a
`https://tu-dominio/api/webhooks/mercadopago`, y copia el secreto de firma a `MERCADOPAGO_WEBHOOK_SECRET`.
3. Pagos únicos usan **Preference** (Checkout Pro); suscripciones usan **PreApproval**. Ambos flujos están
implementados en `lib/billing/mercadopago.ts` con verificación de firma `x-signature` en el webhook.

## 9. Configuración de OpenAI

Define `OPENAI_API_KEY`. Se usa para:

- Generar las 3 variantes de contenido de LinkedIn (`lib/linkedin/content-generator.ts`)
- (Puntos de extensión ya cableados en el dominio) Chatbot RAG, documentación técnica asistida, etc.

Sin esta variable, los endpoints que la requieren devuelven un error explícito — nunca generan contenido simulado.

## 10. Configuración de LinkedIn

1. Crea una app en [LinkedIn Developer Portal](https://www.linkedin.com/developers/apps) con el producto
*Share on LinkedIn* / *Community Management API* habilitado sobre tu Company Page.
2. Completa `LINKEDIN_CLIENT_ID`, `LINKEDIN_CLIENT_SECRET`, `LINKEDIN_ORGANIZATION_ID` (URN numérico de tu página).
3. Autoriza la app vía OAuth 2.0 (flujo estándar de LinkedIn) para obtener `LINKEDIN_ACCESS_TOKEN`.
4. Sin estas variables, `/admin/linkedin` funciona igual para generar y aprobar contenido, pero la publicación real
falla con `LinkedInNotConfiguredError` — nunca se marca como publicado sin que la API realmente responda 200.
5. El cron (`/api/cron/linkedin`, lunes/miércoles/viernes 09:00 hora de Chile — ver `vercel.json`) **solo publica
contenido en estado `SCHEDULED`**, es decir, ya aprobado por un administrador.

## 11. Variables de entorno

Ver `.env.example` para la lista completa. Resumen:

| Variable | Uso |
|---|---|
| `NEXT_PUBLIC_APP_URL` | URL pública de la app (SEO, redirects de checkout) |
| `NEXT_PUBLIC_SUPABASE_URL` / `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Cliente Supabase (browser + server) |
| `SUPABASE_SERVICE_ROLE_KEY` | Operaciones de confianza server-side (nunca al cliente) |
| `DATABASE_URL` / `DIRECT_URL` | Conexión Prisma (pooler / directa) |
| `STRIPE_*` | Checkout, portal y webhooks de Stripe |
| `MERCADOPAGO_*` | Preferencias, suscripciones y webhooks de MercadoPago |
| `OPENAI_API_KEY` | Generación de contenido con IA |
| `LINKEDIN_*` | Adaptador de publicación en LinkedIn |
| `ENCRYPTION_KEY` | Cifra credenciales de integraciones (AES-256-GCM). Generar con `openssl rand -base64 32` |
| `CRON_SECRET` | Protege `/api/cron/*` |

## 12. Ejecución local

```bash
npm run dev
```

Abre [http://localhost:3000](http://localhost:3000). Rutas principales:

- `/` — landing pública
- `/servicios`, `/servicios/[slug]`, `/precios`, `/blog` — catálogo y contenido público
- `/login`, `/registro` — autenticación
- `/onboarding` — creación de organización tras el registro
- `/dashboard` — panel del cliente (requiere sesión + organización)
- `/admin` — panel administrativo (requiere rol `ADMIN`)

## 13. Pruebas

```bash
npm run test # Unit + integration (Vitest) — no requieren base de datos real
npm run test:watch # Modo watch
npm run test:e2e # Playwright — requiere `npm run dev` corriendo (o webServer automático)
```

- **Unit**: `tests/unit/*` — utilidades, catálogo de 20 servicios, límites de plan, cifrado, rate limiting,
scheduler de LinkedIn, protección del cron.
- **Integration**: `tests/integration/*` — aislamiento multi-tenant (`assertOrganizationAccess`) e idempotencia de
webhooks, con Prisma mockeado (no requieren Postgres real para correr en CI).
- **E2E crítico**: `tests/e2e/critical-flow.spec.ts` cubre
*Registro → organización → servicio → plan → checkout de prueba → webhook → dashboard → solicitud → resultado*.
Los pasos que dependen de credenciales reales de Supabase/Stripe se **omiten explícitamente** (no se simulan) si
no defines `E2E_SUPABASE_TEST_EMAIL`, `E2E_SUPABASE_TEST_PASSWORD` y `STRIPE_WEBHOOK_SECRET` contra un proyecto de
prueba.

## 14. Despliegue en Vercel

1. Importa el repositorio en Vercel.
2. Configura todas las variables de `.env.example` en **Project Settings → Environment Variables**.
3. Build command: `npx prisma generate && next build` (o agrega `"postinstall": "prisma generate"` a tus scripts).
4. Ejecuta las migraciones contra la base de producción antes del primer despliegue:
```bash
npx prisma migrate deploy
npm run db:seed # opcional, solo si quieres el catálogo pre-cargado
```
5. Los **Cron Jobs** están declarados en `vercel.json` (`/api/cron/linkedin` y `/api/cron/grace-period`) y se
activan automáticamente al desplegar en Vercel. Verifica que `CRON_SECRET` esté configurado — Vercel lo envía
automáticamente como `Authorization: Bearer $CRON_SECRET` en cada invocación.

## 15. Seguridad

- Autorización centralizada en `lib/auth/guards.ts` (`requireUser`, `requireOrganization`, `requireAdmin`,
`assertOrganizationAccess`) — el `organizationId` **nunca** se toma de un valor enviado por el navegador sin
validar contra la sesión y las `Membership` reales.
- Webhooks de Stripe y MercadoPago verifican firma y son idempotentes vía la tabla `WebhookEvent`.
- Credenciales de integraciones (`OrganizationIntegration.encryptedCredentials`) se cifran con AES-256-GCM antes de
persistirse (`lib/security/encryption.ts`).
- Validación de entrada con Zod en todos los endpoints que reciben datos externos.
- Sanitización de texto libre y validación de tipo/tamaño de archivos subidos (`lib/security/sanitize.ts`).
- Rate limiting básico en endpoints públicos sensibles (`lib/security/rate-limit.ts`) — ver limitaciones.
- `AuditLog` registra operaciones administrativas y sensibles.
- Archivos de clientes se sirven exclusivamente vía URLs firmadas de corta duración (nunca URLs públicas).

## 16. Limitaciones conocidas

- El **rate limiting** es en memoria por instancia; en un despliegue serverless multi-instancia (Vercel) no es un
límite global estricto. Para eso, sustituir `lib/security/rate-limit.ts` por Upstash Redis / Vercel KV
manteniendo la misma interfaz (`checkRateLimit`).
- Las integraciones que requieren credenciales reales de terceros (WhatsApp Business API, LinkedIn, OpenAI) están
completamente implementadas (interfaz, persistencia, validaciones, adaptador) pero **no pueden ejecutarse de
extremo a extremo sin esas credenciales** — fallan explícitamente en vez de simular resultados.
- MercadoPago Suscripciones (`PreApproval`) no acepta `notification_url` por request; el webhook de suscripciones
se configura a nivel de aplicación en el panel de MercadoPago.
- El entorno en que se generó este proyecto no tiene una base de datos Postgres real conectada; `npm run build`
compila y tipa correctamente, pero las páginas que consultan Prisma están marcadas `force-dynamic` para no
requerir conexión durante el build — se conectan en tiempo de request contra el `DATABASE_URL` configurado.

## 17. Solución de problemas

| Problema | Causa probable | Solución |
|---|---|---|
| `PrismaClientInitializationError` | `DATABASE_URL`/`DIRECT_URL` no configuradas o Postgres inaccesible | Revisa las credenciales de Supabase y que el proyecto esté activo |
| Webhook de Stripe responde 400 "Firma inválida" | `STRIPE_WEBHOOK_SECRET` no coincide con el de `stripe listen` | Vuelve a copiar el `whsec_...` que imprime la Stripe CLI |
| El login con Google redirige a un error | Redirect URL no registrada en Supabase/Google Cloud | Agrega `https://tu-dominio/auth/callback` en ambos paneles |
| `LinkedInNotConfiguredError` al publicar | Faltan variables `LINKEDIN_*` | Completa la autorización OAuth de LinkedIn (sección 10) |
| El seed falla creando usuarios | `SUPABASE_SERVICE_ROLE_KEY` no configurada | El seed continúa igual creando usuarios de dominio "locales", pero no podrás iniciar sesión con ellos hasta configurar Supabase |

## 18. Comandos

```bash
npm install
npx prisma generate
npx prisma migrate dev
npm run db:seed
npm run dev
npm run test
npm run build
```
12 changes: 12 additions & 0 deletions app/(auth)/actualizar-password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { UpdatePasswordForm } from "@/components/public/update-password-form";
import { buildMetadata } from "@/lib/seo/metadata";

export const metadata = buildMetadata({
title: "Actualizar contraseña",
description: "Define una nueva contraseña para tu cuenta.",
path: "/actualizar-password",
});

export default function UpdatePasswordPage() {
return <UpdatePasswordForm />;
}
Loading