# Mercado Minero — Diseño Fase 2: Autenticación + Publicación de activos

- **Fecha:** 2026-06-03
- **Estado:** Aprobado (diseño) — pendiente de plan de implementación
- **Depende de:** Fase 1 (catálogo) ya implementada. Ver `2026-06-03-marketplace-catalog-design.md`.

## 1. Objetivo y alcance

Construir la **parte interna protegida por contraseña**: que un usuario pueda **registrarse, iniciar sesión** y, ya autenticado, **publicar un activo minero** mediante el asistente de 7 pasos visto en las capturas (`/Capturas`). El activo publicado se guarda en la base y **aparece en el catálogo público** (`/marketplace`).

### Dentro de alcance (Fase 2)
- Registro (`/registro`) e inicio de sesión (`/login`) con email + contraseña.
- Sesiones seguras (cookie httpOnly) y protección de rutas `/dashboard/*` con `middleware.ts`.
- Dashboard de inicio (`/dashboard`) con bienvenida y acceso al asistente.
- Asistente "Publicar Activo Minero" (`/dashboard/publicar-activo`) de 7 pasos.
- `createListing`: crea el `Listing` con `ownerId` y `status=PUBLISHED`; queda visible en el catálogo.
- Header adaptado al estado de sesión (logueado / no logueado) + cerrar sesión.

### Fuera de alcance (fases siguientes)
- Subida real de archivos (portada, galería, documentos): la UI de carga se muestra, pero la portada usa el thumbnail generado por `imageSeed`; no se almacenan archivos.
- "Generar con IA" real: el botón usa un generador por plantillas (sin API ni costo).
- Sección "mis publicaciones" (listar/editar/borrar lo propio).
- Acciones del comprador: "Expresar interés", "Solicitar info pack", documentos post-NDA.
- Roles avanzados (BUYER, ADMIN, agentes), verificación de identidad, recuperación de contraseña por email.

## 2. Enfoque de autenticación (Enfoque A)

Auth propia, sin dependencias nativas:
- **Hash de contraseña:** `crypto.scryptSync` + sal aleatoria (`crypto.randomBytes`), formato almacenado `salt:hash` (hex). Verificación con comparación en tiempo constante (`crypto.timingSafeEqual`).
- **Sesión:** token aleatorio (`crypto.randomBytes(32).toString('hex')`) guardado en tabla `Session` con `expiresAt` (30 días). Se entrega en cookie `mm_session` httpOnly, `sameSite=lax`, `secure` en producción, `path=/`.
- **Protección de rutas:** `middleware.ts` redirige a `/login?next=<ruta>` si no hay cookie de sesión al acceder a `/dashboard/*`. Además, cada página/acción del dashboard valida la sesión en el servidor con `getCurrentUser()` (defensa en profundidad; el middleware sólo comprueba presencia de cookie, no validez).
- **Server Actions** para `register`, `login`, `logout` (usan `cookies()` de `next/headers`).

> No se usa librería de auth (Auth.js) para evitar fricción de beta con Next 16, igual que se fijó Prisma 6.

## 3. Modelo de datos

### Nuevos modelos
```
model User {
  id           String    @id @default(cuid())
  email        String    @unique
  passwordHash String
  name         String
  company      String?
  role         String    @default("SELLER") // SELLER | BUYER | ADMIN (futuro)
  createdAt    DateTime  @default(now())
  sessions     Session[]
  listings     Listing[]
}

model Session {
  id        String   @id @default(cuid())
  token     String   @unique
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  userId    String
  expiresAt DateTime
  createdAt DateTime @default(now())
  @@index([userId])
}
```

### Extensión de `Listing`
Campos nuevos (todos opcionales salvo `status`), para no romper las semillas existentes:
- `ownerId String?` + relación `owner User? @relation(...)`.
- `status String @default("PUBLISHED")` // DRAFT | PUBLISHED
- `offerType String?` // VENTA_TOTAL | VENTA_PARCIAL (tipo de oferta, paso 1)
- `headline String?` // frase gancho / headline técnico
- `sellReason String?` // motivo de la venta
- `sellerCompany String?`, `sellerWebsite String?`
- `sellerType String?` // PERSONA_NATURAL | PERSONA_JURIDICA | ESTADO
- `mineralizationType String?`, `works String?` // geología
- `waterAccess String?`, `energyAccess String?`
- `esgEnvStatus String?`, `esgClosurePlan String?`, `esgCommunity String?`, `esgLiabilities String?`
- `contactName String?`, `contactRole String?`, `contactEmail String?`, `contactPhone String?`, `contactPreference String?` // EMAIL | TELEFONO | WHATSAPP
- `hasAgent Boolean @default(false)`

El catálogo (`getListings`) pasa a filtrar `status = "PUBLISHED"`. Migración Prisma nueva (`add_auth_and_listing_fields`).

## 4. Constantes nuevas (`src/lib/constants.ts`)
- `OFFER_TYPES` = VENTA_TOTAL, VENTA_PARCIAL (+ labels).
- `SELLER_TYPES` = PERSONA_NATURAL, PERSONA_JURIDICA, ESTADO (+ labels).
- `CONTACT_PREFERENCES` = EMAIL, TELEFONO, WHATSAPP (+ labels).
- `MINERALIZATION_TYPES` y `WORKS_OPTIONS` (trabajos realizados) como listas con labels.

## 5. Capa de auth (`src/lib/auth.ts`)
- `hashPassword(plain): string` y `verifyPassword(plain, stored): boolean` (scrypt + timingSafeEqual).
- `createSession(userId): token` (crea fila Session, devuelve token).
- `getSessionUser(token): User | null` (busca sesión válida no expirada → usuario).
- `getCurrentUser(): Promise<User | null>` (lee cookie `mm_session`, delega en `getSessionUser`).
- `deleteSession(token)`.

## 6. Server Actions (`src/app/(auth)/actions.ts`)
- `registerAction(formData)`: valida email/única/contraseña (mín 8), crea User (hash), crea sesión, setea cookie, redirige a `/dashboard`.
- `loginAction(formData)`: valida credenciales, crea sesión, cookie, redirige a `next` o `/dashboard`. Mensaje de error genérico si falla.
- `logoutAction()`: borra sesión + cookie, redirige a `/`.
- `createListingAction(payload)`: requiere `getCurrentUser()`; valida con `buildListingInput` (ver §8) y crea el Listing + commodities + dealTypes con `ownerId`. Redirige a `/marketplace/<slug>`.

## 7. Páginas y componentes
| Ruta | Descripción |
|---|---|
| `/registro` | Formulario de registro (nombre, empresa opcional, email, contraseña). Server action. |
| `/login` | Formulario de inicio de sesión (email, contraseña). Soporta `?next=`. |
| `/dashboard` | Inicio protegido: saludo, tarjeta para "Publicar activo minero". |
| `/dashboard/publicar-activo` | Asistente de 7 pasos (componente cliente). |

- `middleware.ts` en la raíz de `src/` (o raíz del proyecto) con `matcher` `['/dashboard/:path*']`.
- `Header` adaptado: si `getCurrentUser()` es null → enlace "Iniciar sesión"; si hay usuario → menú con nombre, "Publicar" (→ `/dashboard/publicar-activo`) y "Cerrar sesión" (logoutAction). El Header pasa a ser server component que lee el usuario.

### Asistente (`src/components/dashboard/PublishWizard.tsx`, cliente)
- Mantiene `step` (0–6) y un objeto `form` con todos los campos.
- Sub-componentes por paso: `StepIdentificacion`, `StepUbicacion`, `StepNegocio`, `StepFichaTecnica`, `StepNarrativa`, `StepContacto`, `StepRevision`.
- Barra de progreso con los 7 pasos numerados (como en las capturas), columna lateral derecha con "¿Por qué publicar?", "Etapas", "Lineamientos", "¿Qué sucede después?" (contenido estático de las capturas).
- Botones "Atrás"/"Siguiente"; en el último paso "Publicar" → `createListingAction`.
- Botones "Generar con IA" (headline y resumen): función local `generarTextoPlantilla(form)` que arma un texto a partir de los campos ya ingresados (sin red).

## 8. Validación / armado (`src/lib/publish.ts`)
- `buildListingInput(form): { ok: true, data } | { ok: false, errors }`: valida campos mínimos (título/nombre, categoría=ACTIVO, país, al menos 1 commodity, al menos 1 estructura de trato), genera el `slug` base (slugify del título) y arma el objeto para crear el Listing (incluye relaciones commodities/dealTypes y campos extra). Función pura y testeable. La **unicidad del slug** se resuelve en `createListingAction`: se comprueba contra la BD y, si ya existe, se le añade un sufijo corto (`crypto.randomBytes(3).toString('hex')`).

## 9. Flujo de datos
- Registro/login: form → server action → `lib/auth` → Prisma (User/Session) → cookie → redirect.
- Dashboard: server component lee `getCurrentUser()`; si null, redirect a `/login` (además del middleware).
- Publicar: wizard (cliente) acumula estado → `createListingAction(payload)` (servidor) → `buildListingInput` → Prisma crea Listing → redirect al detalle. El catálogo lo muestra por `status=PUBLISHED`.

## 10. Manejo de errores
- Registro: email duplicado → mensaje claro; contraseña corta → mensaje.
- Login: credenciales inválidas → mensaje genérico ("Email o contraseña incorrectos").
- Asistente: validación por paso antes de avanzar; en "Publicar", si `buildListingInput` falla, muestra errores sin perder lo ingresado.
- Rutas protegidas sin sesión → redirect a `/login?next=`.

## 11. Pruebas (TDD)
- `lib/auth`: `hashPassword`/`verifyPassword` round-trip (acepta correcta, rechaza incorrecta); formato `salt:hash`.
- `lib/auth`: `createSession` + `getSessionUser` devuelve el usuario; sesión expirada → null. (Integración con DB de prueba.)
- `lib/publish`: `buildListingInput` valida mínimos, genera slug, arma relaciones; rechaza payload incompleto.
- Humo en la app: registro → login → publicar → ver el activo en `/marketplace`.

## 12. Criterios de éxito
1. Un visitante puede registrarse, cerrar sesión e iniciar sesión.
2. `/dashboard/*` no es accesible sin sesión (redirige a `/login`).
3. El asistente de 7 pasos permite completar y publicar un activo.
4. El activo publicado aparece en `/marketplace` y su detalle es accesible.
5. El Header refleja el estado de sesión.
6. Las pruebas unitarias/integración pasan; `npm run build` exitoso.
