diff --git a/DOCUMENTACION.md b/DOCUMENTACION.md
new file mode 100644
index 0000000..2c5ee45
--- /dev/null
+++ b/DOCUMENTACION.md
@@ -0,0 +1,1192 @@
+# π DocumentaciΓ³n Completa del Proyecto - Sistema de GestiΓ³n de Stock
+
+## Γndice
+1. [IntroducciΓ³n](#1-introducciΓ³n)
+2. [Arquitectura General](#2-arquitectura-general)
+3. [Frontend (Interfaz de Usuario)](#3-frontend-interfaz-de-usuario)
+4. [Backend (Servidor)](#4-backend-servidor)
+5. [Base de Datos](#5-base-de-datos)
+6. [AutenticaciΓ³n](#6-autenticaciΓ³n)
+7. [API Gateway](#7-api-gateway)
+8. [Docker y Contenedores](#8-docker-y-contenedores)
+9. [Flujo Completo de una OperaciΓ³n](#9-flujo-completo-de-una-operaciΓ³n)
+10. [Glosario de TΓ©rminos](#10-glosario-de-tΓ©rminos)
+
+---
+
+## 1. IntroducciΓ³n
+
+### ΒΏQuΓ© es este proyecto?
+Este proyecto es un **sistema de gestiΓ³n de inventario/stock** desarrollado como Trabajo PrΓ‘ctico Integrador (TPI) para la materia Desarrollo de Software. Permite a los usuarios:
+
+- π¦ **Gestionar productos**: crear, editar, ver y eliminar productos
+- π·οΈ **Organizar por categorΓas**: agrupar productos en diferentes categorΓas
+- π **Manejar reservas**: reservar productos con control de stock
+- π **Control de acceso**: solo usuarios autenticados pueden modificar datos
+
+### TecnologΓas utilizadas
+
+| Componente | TecnologΓa | ΒΏPara quΓ© sirve? |
+|------------|------------|------------------|
+| Frontend | Next.js + React | La interfaz visual que ven los usuarios |
+| Backend | Node.js + Express | El servidor que procesa las operaciones |
+| Base de datos | Supabase (PostgreSQL) | Donde se guardan todos los datos |
+| AutenticaciΓ³n | Keycloak | Maneja los usuarios y contraseΓ±as |
+| API Gateway | Nginx | Redirige las peticiones al lugar correcto |
+| Contenedores | Docker | Empaqueta todo para que funcione igual en cualquier computadora |
+
+---
+
+## 2. Arquitectura General
+
+### ΒΏCΓ³mo funciona todo junto?
+
+Imagina el sistema como un restaurante:
+
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β π INTERNET (Usuario) β
+ββββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
+ β
+ βΌ
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β πͺ API GATEWAY (Nginx - Puerto 80) β
+β "El recepcionista que dirige a cada visitante" β
+βββββββββ¬ββββββββββββββββββββ¬ββββββββββββββββββββ¬ββββββββββββββββββββββββββ
+ β β β
+ βΌ βΌ βΌ
+βββββββββββββββββ βββββββββββββββββ βββββββββββββββββββββ
+β π₯οΈ FRONTEND β β βοΈ BACKEND β β π KEYCLOAK β
+β (Next.js) β β (Express) β β (AutenticaciΓ³n) β
+β Puerto 3000 β β Puerto 4000 β β Puerto 8081 β
+βββββββββββββββββ βββββββββ¬ββββββββ βββββββββββββββββββββ
+ β
+ βΌ
+ βββββββββββββββββ
+ β ποΈ SUPABASE β
+ β (Base datos) β
+ βββββββββββββββββ
+```
+
+### AnalogΓa con un restaurante
+
+- **Frontend** = El menΓΊ y las mesas donde se sienta el cliente
+- **API Gateway** = El recepcionista que te dirige a la mesa correcta
+- **Backend** = La cocina donde se preparan los pedidos
+- **Base de datos** = La despensa donde estΓ‘n todos los ingredientes
+- **Keycloak** = El guardia que verifica tu reservaciΓ³n antes de dejarte entrar
+
+---
+
+## 3. Frontend (Interfaz de Usuario)
+
+### ΒΏQuΓ© es el Frontend?
+Es todo lo que el usuario **ve y toca** en su navegador. EstΓ‘ construido con **Next.js**, que es un framework basado en **React**.
+
+### Estructura de carpetas
+```
+frontend/
+βββ src/
+β βββ app/ # PΓ‘ginas de la aplicaciΓ³n
+β β βββ page.tsx # PΓ‘gina de login (inicio)
+β β βββ dashboard/ # Panel principal despuΓ©s de login
+β β βββ producto/ # PΓ‘ginas de productos
+β β βββ categorias/ # PΓ‘ginas de categorΓas
+β β βββ reservas/ # PΓ‘ginas de reservas
+β β
+β βββ componentes/ # Piezas reutilizables de la interfaz
+β β βββ ListaProductos.tsx # Tabla que muestra los productos
+β β βββ ProductoForm.tsx # Formulario para crear/editar productos
+β β βββ GestionCategorias.tsx
+β β βββ GestionReservas.tsx
+β β βββ KeycloakProvider.tsx # Maneja la autenticaciΓ³n
+β β βββ SlidePanel.tsx # Panel deslizante lateral
+β β
+β βββ servicios/
+β β βββ api.js # Funciones para comunicarse con el backend
+β β
+β βββ lib/
+β βββ keycloak.js # ConfiguraciΓ³n de autenticaciΓ³n
+```
+
+### ExplicaciΓ³n de archivos clave
+
+#### π `page.tsx` (PΓ‘gina de Login)
+```typescript
+// Esta es la pΓ‘gina principal de login
+import { useState } from 'react'
+import { useRouter } from 'next/navigation' // Importar el hook de navegaciΓ³n
+
+export default function Page() {
+ // useRouter: hook de Next.js para navegar entre pΓ‘ginas
+ const router = useRouter()
+
+ // Estados: son como "variables especiales" que React recuerda
+ const [email, setEmail] = useState('') // Guarda el email que escribe el usuario
+ const [password, setPassword] = useState('') // Guarda la contraseΓ±a
+ const [isLoading, setIsLoading] = useState(false) // ΒΏEstΓ‘ cargando?
+ const [error, setError] = useState('') // Mensaje de error si algo falla
+
+ // Esta funciΓ³n se ejecuta cuando el usuario hace click en "Iniciar SesiΓ³n"
+ const handleLogin = async (e) => {
+ e.preventDefault() // Evita que la pΓ‘gina se recargue
+ setIsLoading(true) // Muestra indicador de carga
+
+ // Intenta autenticar con Keycloak
+ // ... cΓ³digo de autenticaciΓ³n ...
+
+ router.push('/dashboard') // Si todo va bien, navega al dashboard
+ }
+
+ // El "return" define quΓ© se ve en pantalla (HTML con esteroides)
+ return (
+
+
+
+ )
+}
+```
+
+**Conceptos importantes:**
+- `useState`: Es como una caja donde guardamos informaciΓ³n que puede cambiar. Cuando cambia, React actualiza automΓ‘ticamente lo que se ve en pantalla.
+- `async/await`: Permite esperar a que algo termine (como una llamada al servidor) antes de continuar.
+- `router.push()`: Navega a otra pΓ‘gina sin recargar todo el navegador.
+
+#### π `ListaProductos.tsx` (Componente de lista)
+```typescript
+export default function ListaProductos() {
+ // Estado para guardar la lista de productos
+ const [productos, setProductos] = useState([])
+ const [cargando, setCargando] = useState(true)
+
+ // useEffect: se ejecuta cuando el componente aparece en pantalla
+ useEffect(() => {
+ // Llamamos al backend para obtener los productos
+ obtenerProductos()
+ .then((datos) => {
+ setProductos(datos) // Guardamos los productos
+ })
+ .finally(() => {
+ setCargando(false) // Ya terminΓ³ de cargar
+ })
+ }, []) // El [] significa "solo ejecutar una vez al inicio"
+
+ // Si estΓ‘ cargando, mostramos un mensaje
+ if (cargando) return Cargando...
+
+ // Mostramos la tabla de productos
+ return (
+
+
+
+ | ID |
+ Nombre |
+ Precio |
+
+
+
+ {/* .map() recorre cada producto y crea una fila */}
+ {productos.map((producto) => (
+
+ | {producto.id} |
+ {producto.nombre} |
+ ${producto.precio} |
+
+ ))}
+
+
+ )
+}
+```
+
+**Conceptos importantes:**
+- `useEffect`: Es como un "evento de inicio". Se ejecuta cuando el componente aparece o cuando algo cambia.
+- `.map()`: Recorre un array y transforma cada elemento en algo visual.
+- `key`: Identificador ΓΊnico que React necesita para saber quΓ© elementos actualizar.
+
+#### π `api.js` (ComunicaciΓ³n con el servidor)
+```javascript
+// URL base del servidor
+const API_URL = process.env.NEXT_PUBLIC_API_URL
+
+// FunciΓ³n auxiliar que agrega autenticaciΓ³n a todas las llamadas
+async function fetchConAuth(endpoint, options = {}) {
+ // Obtener el token de autenticaciΓ³n
+ const token = keycloak?.token
+
+ // Preparar los headers (informaciΓ³n adicional que va con la peticiΓ³n)
+ const headers = new Headers(options.headers || {})
+
+ // Si hay datos que enviar, indicamos que son JSON
+ if (options.body) {
+ headers.append('Content-Type', 'application/json')
+ }
+
+ // Agregamos el token de autenticaciΓ³n
+ if (token) {
+ headers.append('Authorization', `Bearer ${token}`)
+ }
+
+ // Hacer la peticiΓ³n al servidor
+ const response = await fetch(`${API_URL}${endpoint}`, {
+ ...options,
+ headers: headers
+ })
+
+ // Si hubo error, lanzamos una excepciΓ³n
+ if (!response.ok) {
+ throw new Error(`Error ${response.status}`)
+ }
+
+ // Devolvemos los datos en formato JSON
+ return response.json()
+}
+
+// Funciones especΓficas para cada operaciΓ³n
+export async function obtenerProductos() {
+ return fetchConAuth('/productos')
+}
+
+export async function agregarProducto(datosProducto) {
+ return fetchConAuth('/productos', {
+ method: 'POST', // POST = crear algo nuevo
+ body: JSON.stringify(datosProducto) // Convertir objeto a texto JSON
+ })
+}
+
+export async function eliminarProducto(id) {
+ return fetchConAuth(`/productos/${id}`, {
+ method: 'DELETE' // DELETE = borrar algo
+ })
+}
+```
+
+**Conceptos importantes:**
+- `fetch`: FunciΓ³n del navegador para hacer peticiones HTTP al servidor.
+- `JSON.stringify()`: Convierte un objeto JavaScript a texto JSON para enviarlo.
+- **MΓ©todos HTTP**:
+ - `GET` = obtener datos
+ - `POST` = crear algo nuevo
+ - `PATCH` = actualizar parcialmente
+ - `DELETE` = eliminar
+
+---
+
+## 4. Backend (Servidor)
+
+### ΒΏQuΓ© es el Backend?
+Es el programa que corre en el servidor y se encarga de:
+- Recibir peticiones del frontend
+- Procesar la lΓ³gica de negocio
+- Guardar/obtener datos de la base de datos
+- Verificar que el usuario tenga permiso
+
+### Estructura de carpetas
+```
+mi-app-backend/
+βββ index.js # Archivo principal que inicia el servidor
+βββ dbConfig.js # ConexiΓ³n a la base de datos
+βββ keycloak-config.js # ConfiguraciΓ³n de autenticaciΓ³n
+β
+βββ Rutas/ # Define las URLs disponibles
+β βββ productosRoutes.js
+β βββ categoriasRoutes.js
+β βββ reservasRoutes.js
+β
+βββ Controladores/ # Maneja las peticiones
+β βββ productosController.js
+β βββ categoriasController.js
+β βββ reservasController.js
+β
+βββ Servicios/ # LΓ³gica de negocio y base de datos
+ βββ productosService.js
+ βββ categoriasService.js
+ βββ reservasService.js
+```
+
+### PatrΓ³n de arquitectura: Rutas β Controladores β Servicios
+
+```
+ PETICIΓN HTTP RUTAS CONTROLADOR SERVICIO
+ β β β β
+ GET /productos ββββββββββΊ "ΒΏQuΓ© ruta es?" βββββββΊ "Validar datos" βββββββΊ "Ir a la BD"
+ β β β β
+ β productosRoutes.js productosController.js productosService.js
+ β β β β
+ ββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββ
+ RESPUESTA JSON
+```
+
+### ExplicaciΓ³n de archivos clave
+
+#### π `index.js` (Punto de entrada del servidor)
+```javascript
+// Importar las librerΓas necesarias
+import express from 'express' // Framework para crear servidores web
+import cors from 'cors' // Permite que el frontend se comunique con nosotros
+
+// Crear la aplicaciΓ³n
+const app = express()
+const PORT = 4000
+
+// MIDDLEWARES: funciones que se ejecutan en CADA peticiΓ³n
+app.use(express.json()) // Permite leer datos JSON del body
+app.use(cors()) // Permite peticiones desde otros dominios
+
+// Configurar autenticaciΓ³n con Keycloak
+app.use(keycloak.middleware())
+
+// RUTAS: conectamos las URLs con sus manejadores
+app.use('/api/v1/productos', productosRouter) // /api/v1/productos/* β productosRouter
+app.use('/api/v1/categorias', categoriasRouter) // /api/v1/categorias/* β categoriasRouter
+app.use('/api/v1/reservas', reservasRouter) // /api/v1/reservas/* β reservasRouter
+
+// Health check: ruta simple para verificar que el servidor estΓ‘ vivo
+app.get('/', (req, res) => {
+ res.json({ mensaje: 'Β‘El servidor estΓ‘ vivo!' })
+})
+
+// INICIAR el servidor
+app.listen(PORT, () => {
+ console.log(`Servidor corriendo en http://localhost:${PORT}`)
+})
+```
+
+**Conceptos importantes:**
+- `express()`: Crea una aplicaciΓ³n web que puede recibir peticiones HTTP.
+- `app.use()`: Agrega un "middleware" (funciΓ³n que se ejecuta antes de las rutas).
+- `app.listen()`: Inicia el servidor y lo deja escuchando peticiones.
+
+#### π `productosRoutes.js` (DefiniciΓ³n de URLs)
+```javascript
+import express from 'express'
+import productosControlador from '../Controladores/productosController.js'
+import { keycloak } from '../keycloak-config.js'
+
+const router = express.Router()
+
+// GET /productos - Obtener todos los productos (pΓΊblico)
+router.get('/', productosControlador.listarProductos)
+
+// GET /productos/:productoId - Obtener un producto especΓfico (pΓΊblico)
+router.get('/:productoId', productosControlador.obtenerProductoPorId)
+
+// POST /productos - Crear producto (protegido - requiere login)
+router.post('/', keycloak.protect(), productosControlador.crearProducto)
+
+// PATCH /productos/:productoId - Actualizar producto (protegido)
+router.patch('/:productoId', keycloak.protect(), productosControlador.actualizarProducto)
+
+// DELETE /productos/:productoId - Eliminar producto (protegido)
+router.delete('/:productoId', keycloak.protect(), productosControlador.eliminarProducto)
+
+export default router
+```
+
+**Conceptos importantes:**
+- `router.get()`, `.post()`, etc.: Definen quΓ© funciΓ³n ejecutar para cada tipo de peticiΓ³n.
+- `:productoId`: Es un parΓ‘metro dinΓ‘mico. Si la URL es `/productos/5`, entonces `productoId = 5`.
+- `keycloak.protect()`: Middleware que verifica que el usuario estΓ© autenticado antes de continuar.
+
+#### π `productosController.js` (Controlador)
+```javascript
+import productosServicio from '../Servicios/productosService.js'
+
+// Controlador para LISTAR productos
+const listarProductos = async (req, res) => {
+ try {
+ // 1. Obtener parΓ‘metros de la URL (?page=1&limit=10)
+ const { page, limit, q } = req.query
+
+ // 2. Llamar al servicio
+ const productos = await productosServicio.listarProductos({ page, limit, q })
+
+ // 3. Responder con Γ©xito (cΓ³digo 200)
+ res.status(200).json(productos)
+
+ } catch (error) {
+ // Si algo falla, responder con error (cΓ³digo 500)
+ console.error('Error:', error)
+ res.status(500).json({ mensaje: 'Error interno del servidor' })
+ }
+}
+
+// Controlador para CREAR producto
+const crearProducto = async (req, res) => {
+ try {
+ // 1. Obtener datos del body (lo que envΓa el frontend)
+ const datosProducto = req.body
+
+ // 2. VALIDAR los datos
+ const { nombre, precio, stockInicial } = datosProducto
+ if (!nombre || precio === undefined || stockInicial === undefined) {
+ // Si faltan datos, devolver error 400 (peticiΓ³n incorrecta)
+ return res.status(400).json({
+ mensaje: 'Faltan datos obligatorios'
+ })
+ }
+
+ // 3. Llamar al servicio para crear el producto
+ const productoCreado = await productosServicio.crearProducto(datosProducto)
+
+ // 4. Responder con Γ©xito (cΓ³digo 201 = creado)
+ res.status(201).json(productoCreado)
+
+ } catch (error) {
+ res.status(500).json({ mensaje: 'Error interno del servidor' })
+ }
+}
+
+export default {
+ listarProductos,
+ crearProducto,
+ // ... mΓ‘s funciones
+}
+```
+
+**Conceptos importantes:**
+- `req` (request): Contiene toda la informaciΓ³n de la peticiΓ³n (URL, body, headers, etc.)
+- `res` (response): Se usa para enviar la respuesta al cliente
+- `async/await`: Permite esperar operaciones asΓncronas (como consultas a BD)
+- **CΓ³digos HTTP**: 200=OK, 201=Creado, 400=Error del cliente, 404=No encontrado, 500=Error del servidor
+
+#### π `productosService.js` (LΓ³gica de negocio)
+```javascript
+import supabase from '../dbConfig.js'
+
+// FunciΓ³n auxiliar para transformar datos de la BD al formato de la API
+const _mapProductoToOutput = (data) => {
+ if (!data) return null
+
+ return {
+ id: data.id,
+ nombre: data.nombre,
+ descripcion: data.descripcion,
+ precio: parseFloat(data.precio_unitario), // La BD usa snake_case
+ stockDisponible: data.stock_disponible, // La API usa camelCase
+ // ... mΓ‘s campos
+ }
+}
+
+// Servicio para LISTAR productos
+const listarProductos = async (filtros) => {
+ const { page = 1, limit = 10, q } = filtros
+
+ // 1. Construir la consulta a Supabase
+ let query = supabase
+ .from('productos') // Tabla 'productos'
+ .select('*') // Seleccionar todas las columnas
+
+ // 2. Aplicar filtro de bΓΊsqueda si existe
+ if (q) {
+ // ilike: bΓΊsqueda case-insensitive (ignora mayΓΊsculas/minΓΊsculas)
+ // El % es un comodΓn que significa "cualquier cosa antes o despuΓ©s"
+ query = query.ilike('nombre', `%${q}%`)
+ }
+
+ // 3. Aplicar paginaciΓ³n
+ const offset = (page - 1) * limit
+ query = query.range(offset, offset + limit - 1)
+
+ // 4. Ejecutar la consulta
+ const { data, error } = await query
+
+ if (error) throw new Error(error.message)
+
+ // 5. Transformar y devolver los datos
+ return data.map(_mapProductoToOutput)
+}
+
+// Servicio para CREAR producto
+const crearProducto = async (datosProducto) => {
+ const { nombre, descripcion, precio, stockInicial } = datosProducto
+
+ // 1. Insertar en la base de datos
+ const { data, error } = await supabase
+ .from('productos')
+ .insert({
+ nombre: nombre,
+ descripcion: descripcion,
+ precio_unitario: precio, // Mapeo a nombre de columna en BD
+ stock_disponible: stockInicial
+ })
+ .select('id') // Devolver el ID del nuevo registro
+ .single() // Esperar un solo resultado
+
+ if (error) throw new Error(error.message)
+
+ // 2. Devolver respuesta de Γ©xito
+ return {
+ id: data.id,
+ mensaje: 'Producto creado exitosamente'
+ }
+}
+
+export default {
+ listarProductos,
+ crearProducto,
+ // ... mΓ‘s funciones
+}
+```
+
+**Conceptos importantes:**
+- `supabase.from('tabla')`: Selecciona la tabla con la que trabajar
+- `.select()`: Define quΓ© columnas obtener
+- `.insert()`: Agrega nuevos registros
+- `.update()`: Modifica registros existentes
+- `.delete()`: Elimina registros
+- `.eq('columna', valor)`: Filtra por igualdad
+- `.single()`: Espera un ΓΊnico resultado
+
+---
+
+## 5. Base de Datos
+
+### ΒΏQuΓ© es Supabase?
+Supabase es una plataforma que proporciona una base de datos PostgreSQL en la nube, junto con una API automΓ‘tica para acceder a los datos.
+
+### Esquema de la base de datos
+
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β PRODUCTOS β
+ββββββββββββββββββ¬βββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββ€
+β id β INTEGER β Identificador ΓΊnico (PK) β
+β nombre β VARCHAR β Nombre del producto β
+β descripcion β TEXT β DescripciΓ³n detallada β
+β precio_unitarioβ DECIMAL β Precio del producto β
+β stock_disponibleβ INTEGER β Cantidad en inventario β
+β dimensiones β JSONB β { largoCm, anchoCm, altoCm } β
+β ubicacion β JSONB β { street, city, state, country } β
+β imagenes β JSONB β Array de URLs de imΓ‘genes β
+β peso_kg β DECIMAL β Peso en kilogramos β
+ββββββββββββββββββ΄βββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββββββ
+ β
+ β N:M
+ βΌ
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β PRODUCTOS_CATEGORIAS β
+ββββββββββββββββββ¬βββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββ€
+β producto_id β INTEGER β FK β productos.id β
+β categoria_id β INTEGER β FK β categorias.id β
+ββββββββββββββββββ΄βββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββββββ
+ β
+ βΌ
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β CATEGORIAS β
+ββββββββββββββββββ¬βββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββ€
+β id β INTEGER β Identificador ΓΊnico (PK) β
+β nombre β VARCHAR β Nombre de la categorΓa β
+β descripcion β TEXT β DescripciΓ³n de la categorΓa β
+ββββββββββββββββββ΄βββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββββββ
+
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β RESERVAS β
+ββββββββββββββββββ¬βββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββ€
+β id β INTEGER β Identificador ΓΊnico (PK) β
+β id_compra β INTEGER β ID de la compra asociada β
+β usuario_id β INTEGER β ID del usuario que reservΓ³ β
+β estado β VARCHAR β 'pendiente', 'confirmado', 'cancelado' β
+β expires_at β TIMESTAMP β Fecha de expiraciΓ³n de la reserva β
+β fecha_creacion β TIMESTAMP β CuΓ‘ndo se creΓ³ β
+β fecha_actualizacionβ TIMESTAMPβ Γltima modificaciΓ³n β
+ββββββββββββββββββ΄βββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββββββ
+ β
+ β 1:N
+ βΌ
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β RESERVAS_PRODUCTOS β
+ββββββββββββββββββ¬βββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββ€
+β reserva_id β INTEGER β FK β reservas.id β
+β producto_id β INTEGER β FK β productos.id β
+β cantidad β INTEGER β Cantidad reservada de ese producto β
+ββββββββββββββββββ΄βββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββββββ
+```
+
+### ConexiΓ³n a Supabase
+
+```javascript
+// dbConfig.js
+import { createClient } from '@supabase/supabase-js'
+
+// URL y clave de Supabase (vienen de variables de entorno)
+const supabaseUrl = process.env.SUPABASE_URL
+const supabaseKey = process.env.SUPABASE_ANON_KEY
+
+// Crear el cliente de Supabase
+const supabase = createClient(supabaseUrl, supabaseKey)
+
+export default supabase
+```
+
+### Ejemplos de consultas
+
+```javascript
+// Obtener todos los productos
+const { data, error } = await supabase
+ .from('productos')
+ .select('*')
+
+// Obtener un producto con sus categorΓas (JOIN)
+// Esta sintaxis especial de Supabase crea un JOIN automΓ‘tico
+// Supabase detecta las Foreign Keys y hace el JOIN por nosotros
+const { data, error } = await supabase
+ .from('productos')
+ .select(`
+ id,
+ nombre,
+ precio_unitario,
+ productos_categorias (
+ categorias (
+ id,
+ nombre
+ )
+ )
+ `)
+ .eq('id', productoId)
+ .single()
+
+// β οΈ IMPORTANTE: El resultado 'data' tendrΓ‘ una estructura ANIDADA:
+// {
+// id: 1,
+// nombre: "Laptop",
+// precio_unitario: 999.99,
+// productos_categorias: [ β Array de relaciones
+// {
+// categorias: { β Objeto anidado de la tabla relacionada
+// id: 5,
+// nombre: "ElectrΓ³nica"
+// }
+// }
+// ]
+// }
+// Por eso necesitamos "mapear" los datos para aplanarlos al formato de la API
+
+// Insertar un nuevo producto
+const { data, error } = await supabase
+ .from('productos')
+ .insert({
+ nombre: 'Producto Nuevo',
+ precio_unitario: 99.99,
+ stock_disponible: 100
+ })
+ .select('id')
+ .single()
+
+// Actualizar un producto
+const { data, error } = await supabase
+ .from('productos')
+ .update({ precio_unitario: 149.99 })
+ .eq('id', productoId)
+
+// Eliminar un producto
+const { data, error } = await supabase
+ .from('productos')
+ .delete()
+ .eq('id', productoId)
+```
+
+---
+
+## 6. AutenticaciΓ³n
+
+### ΒΏQuΓ© es Keycloak?
+Keycloak es un servidor de identidad y acceso. En tΓ©rminos simples, es el "guardia de seguridad" que verifica quiΓ©n eres antes de dejarte hacer ciertas cosas.
+
+### Flujo de autenticaciΓ³n
+
+```
+βββββββββββββββ βββββββββββββββ βββββββββββββββ
+β USUARIO β β FRONTEND β β KEYCLOAK β
+ββββββββ¬βββββββ ββββββββ¬βββββββ ββββββββ¬βββββββ
+ β β β
+ β 1. Escribe email β β
+ β y contraseΓ±a β β
+ ββββββββββββββββββββββ>β β
+ β β β
+ β β 2. EnvΓa credenciales
+ β ββββββββββββββββββββββ>β
+ β β β
+ β β 3. Si son correctas,β
+ β β devuelve TOKEN β
+ β β<ββββββββββββββββββββββ
+ β β β
+ β 4. Redirige al β β
+ β dashboard β β
+ β<ββββββββββββββββββββββ β
+ β β β
+```
+
+### ΒΏQuΓ© es un Token?
+Un **token** es como una "pulsera de acceso" digital. Contiene:
+- QuiΓ©n eres (tu ID de usuario)
+- QuΓ© permisos tienes
+- CuΓ‘ndo expira
+
+Cada vez que haces una peticiΓ³n protegida, envΓas el token para que el servidor verifique que tienes permiso.
+
+### ConfiguraciΓ³n en el Frontend
+
+```javascript
+// keycloak.js
+import Keycloak from 'keycloak-js'
+
+const keycloak = new Keycloak({
+ url: 'http://localhost:8081', // URL de Keycloak
+ realm: 'ds-2025-realm', // "Realm" = grupo de usuarios
+ clientId: 'grupo-02' // ID de nuestra aplicaciΓ³n
+})
+
+export default keycloak
+```
+
+### ProtecciΓ³n de rutas en el Backend
+
+```javascript
+// En productosRoutes.js
+import { keycloak } from '../keycloak-config.js'
+
+// Ruta PΓBLICA (cualquiera puede acceder)
+router.get('/productos', productosController.listar)
+
+// Ruta PROTEGIDA (solo usuarios autenticados)
+router.post('/productos', keycloak.protect(), productosController.crear)
+// ^^^^^^^^^^^^^^^^
+// Este middleware verifica el token
+```
+
+---
+
+## 7. API Gateway
+
+### ΒΏQuΓ© es el API Gateway?
+Es un servidor Nginx que actΓΊa como "recepcionista". Todas las peticiones llegan primero a Γ©l, y Γ©l las redirige al servicio correcto.
+
+### ΒΏPor quΓ© usarlo?
+
+1. **Una sola URL**: El frontend solo necesita conocer `http://localhost` en lugar de mΓΊltiples puertos
+2. **Seguridad**: Oculta los puertos internos de los servicios
+3. **Balanceo**: Puede distribuir carga entre mΓΊltiples servidores
+
+### ConfiguraciΓ³n de Nginx
+
+```nginx
+# nginx.conf
+
+# Definir los servidores "upstream" (backends)
+upstream stock-backend {
+ server stock-backend:4000; # Nuestro backend en puerto 4000
+}
+
+upstream compras-backend {
+ server compras-backend:8000; # Backend de otro grupo
+}
+
+server {
+ listen 80; # Escuchar en puerto 80 (HTTP estΓ‘ndar)
+
+ # Rutas para nuestro backend (Grupo 2 - Stock)
+ location /api/ {
+ proxy_pass http://stock-backend;
+ # Las peticiones a /api/* van al backend de stock
+ }
+
+ location /stock/ {
+ proxy_pass http://stock-backend/;
+ # Las peticiones a /stock/* tambiΓ©n van al backend de stock
+ }
+
+ # Rutas para el grupo de Compras
+ location /compras/ {
+ proxy_pass http://compras-backend/;
+ }
+
+ # Por defecto, servir el frontend
+ location / {
+ proxy_pass http://frontend-server;
+ }
+}
+```
+
+### Flujo de una peticiΓ³n
+
+```
+Usuario escribe: http://localhost/api/v1/productos
+ β
+ βΌ
+ ββββββββββββ
+ β NGINX β βββΊ "La URL empieza con /api/,
+ β (Gateway)β lo envΓo a stock-backend"
+ ββββββ¬ββββββ
+ β
+ βΌ
+ http://stock-backend:4000/api/v1/productos
+ β
+ βΌ
+ ββββββββββββ
+ β BACKEND β βββΊ Procesa la peticiΓ³n
+ β (Express)β
+ ββββββ¬ββββββ
+ β
+ βΌ
+ Respuesta JSON
+```
+
+---
+
+## 8. Docker y Contenedores
+
+### ΒΏQuΓ© es Docker?
+Docker es una herramienta que "empaqueta" aplicaciones con todo lo que necesitan para funcionar, creando **contenedores** aislados.
+
+**AnalogΓa**: Imagina que Docker es como las cajas de mudanza. Cada caja (contenedor) tiene todo lo necesario: los muebles (cΓ³digo), las herramientas (librerΓas) y las instrucciones de montaje (configuraciΓ³n).
+
+### Docker Compose
+Docker Compose permite definir y ejecutar mΓΊltiples contenedores a la vez. El archivo `docker-compose.yml` describe todos los servicios.
+
+### Servicios definidos
+
+```yaml
+# docker-compose.yml
+
+services:
+ # === BASE DE DATOS DE KEYCLOAK ===
+ keycloak-db:
+ image: postgres:15 # Usar imagen de PostgreSQL 15
+ environment:
+ POSTGRES_DB: keycloak # Nombre de la base de datos
+ POSTGRES_USER: keycloak
+ POSTGRES_PASSWORD: keycloak
+
+ # === KEYCLOAK (AUTENTICACIΓN) ===
+ keycloak:
+ image: quay.io/keycloak/keycloak:23.0.6
+ ports:
+ - "8081:8080" # Puerto externo:interno
+ depends_on:
+ - keycloak-db # Esperar a que la BD estΓ© lista
+
+ # === NUESTRO BACKEND ===
+ backend:
+ build:
+ context: ./mi-app-backend # Carpeta con el cΓ³digo
+ dockerfile: Dockerfile # Archivo con instrucciones de construcciΓ³n
+ ports:
+ - "4000:4000"
+ environment:
+ - PORT=4000
+ - SUPABASE_URL=... # Variables de entorno
+ - SUPABASE_ANON_KEY=...
+
+ # === NUESTRO FRONTEND ===
+ frontend:
+ build:
+ context: ./frontend
+ dockerfile: Dockerfile
+ ports:
+ - "3000:3000"
+ depends_on:
+ - backend # Esperar a que el backend estΓ© listo
+
+ # === API GATEWAY ===
+ api-gateway:
+ build:
+ context: ./api-gateway
+ ports:
+ - "80:80" # Puerto 80 (HTTP estΓ‘ndar)
+ depends_on:
+ - backend
+ - frontend
+```
+
+### Dockerfile del Backend
+
+```dockerfile
+# mi-app-backend/Dockerfile
+
+# 1. Usar imagen base de Node.js
+# "alpine" es una versiΓ³n mΓ‘s pequeΓ±a y segura de Linux
+# Es ideal para contenedores porque ocupa menos espacio (~5MB vs ~100MB)
+FROM node:20-alpine
+
+# 2. Crear directorio de trabajo dentro del contenedor
+WORKDIR /app
+
+# 3. Copiar archivos de dependencias
+COPY package*.json ./
+
+# 4. Instalar dependencias
+RUN npm install
+
+# 5. Copiar el resto del cΓ³digo
+COPY . .
+
+# 6. Exponer el puerto que usa la aplicaciΓ³n
+EXPOSE 4000
+
+# 7. Comando para iniciar la aplicaciΓ³n
+CMD ["npm", "start"]
+```
+
+### Comandos ΓΊtiles de Docker
+
+```bash
+# Construir e iniciar todos los servicios
+docker-compose up --build
+
+# Iniciar en segundo plano
+docker-compose up -d
+
+# Ver logs de todos los servicios
+docker-compose logs -f
+
+# Ver logs de un servicio especΓfico
+docker-compose logs -f backend
+
+# Detener todos los servicios
+docker-compose down
+
+# Ver contenedores activos
+docker ps
+```
+
+---
+
+## 9. Flujo Completo de una OperaciΓ³n
+
+### Ejemplo: Crear un nuevo producto
+
+Veamos paso a paso quΓ© sucede cuando un usuario crea un producto:
+
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β PASO 1: INTERFAZ DE USUARIO β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
+β β
+β El usuario llena el formulario: β
+β βββββββββββββββββββββββββββββββββββββββ β
+β β Nombre: [Laptop Dell XPS ] β β
+β β Precio: [$1500 ] β β
+β β Stock: [25 ] β β
+β β CategorΓa: [β ElectrΓ³nica ] β β
+β β β β
+β β [ Agregar Producto ] β β
+β βββββββββββββββββββββββββββββββββββββββ β
+β β
+β Al hacer click, se ejecuta handleSubmit() en ProductoForm.tsx β
+β β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ β
+ βΌ
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β PASO 2: LLAMADA A LA API β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
+β β
+β // En ProductoForm.tsx β
+β const handleSubmit = async () => { β
+β await agregarProducto(formData) // β Esto llama a api.js β
+β } β
+β β
+β // En api.js β
+β export async function agregarProducto(datos) { β
+β return fetchConAuth('/productos', { β
+β method: 'POST', β
+β body: JSON.stringify(datos) β
+β }) β
+β } β
+β β
+β Se envΓa: POST http://localhost/api/v1/productos β
+β Con body: { nombre: "Laptop Dell XPS", precio: 1500, ... } β
+β Con header: Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cC... β
+β β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ β
+ βΌ
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β PASO 3: API GATEWAY β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
+β β
+β Nginx recibe: POST /api/v1/productos β
+β β
+β La regla "location /api/" coincide β
+β β Redirige a: http://stock-backend:4000/api/v1/productos β
+β β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ β
+ βΌ
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β PASO 4: RUTAS (EXPRESS) β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
+β β
+β // En index.js β
+β app.use('/api/v1/productos', productosRouter) β
+β β
+β // En productosRoutes.js β
+β router.post('/', keycloak.protect(), productosControlador.crearProducto)β
+β β β
+β Primero verifica el token de autenticaciΓ³n β
+β Si es vΓ‘lido, continΓΊa al controlador β
+β β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ β
+ βΌ
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β PASO 5: CONTROLADOR β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
+β β
+β // En productosController.js β
+β const crearProducto = async (req, res) => { β
+β // 1. Extraer datos del body β
+β const { nombre, precio, stockInicial } = req.body β
+β β
+β // 2. Validar datos obligatorios β
+β if (!nombre || precio === undefined) { β
+β return res.status(400).json({ error: 'Faltan datos' }) β
+β } β
+β β
+β // 3. Llamar al servicio β
+β const resultado = await productosServicio.crearProducto(req.body) β
+β β
+β // 4. Responder β
+β res.status(201).json(resultado) β
+β } β
+β β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ β
+ βΌ
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β PASO 6: SERVICIO β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
+β β
+β // En productosService.js β
+β const crearProducto = async (datos) => { β
+β // 1. Insertar en Supabase β
+β const { data, error } = await supabase β
+β .from('productos') β
+β .insert({ β
+β nombre: datos.nombre, β
+β precio_unitario: datos.precio, β
+β stock_disponible: datos.stockInicial β
+β }) β
+β .select('id') β
+β .single() β
+β β
+β // 2. Insertar categorΓas si existen β
+β if (datos.categoriaIds) { β
+β await supabase.from('productos_categorias').insert(...) β
+β } β
+β β
+β // 3. Devolver resultado β
+β return { id: data.id, mensaje: 'Producto creado' } β
+β } β
+β β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ β
+ βΌ
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β PASO 7: RESPUESTA β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
+β β
+β La respuesta viaja de vuelta: β
+β β
+β Supabase β Servicio β Controlador β Express β Nginx β Frontend β
+β β
+β El frontend recibe: β
+β { β
+β "id": 42, β
+β "mensaje": "Producto creado exitosamente" β
+β } β
+β β
+β El frontend muestra: "Β‘Producto agregado correctamente!" β
+β Y actualiza la lista de productos. β
+β β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+```
+
+---
+
+## 10. Glosario de TΓ©rminos
+
+### TΓ©rminos Generales
+
+| TΓ©rmino | DefiniciΓ³n |
+|---------|------------|
+| **API** | Application Programming Interface. Conjunto de reglas que permite que diferentes programas se comuniquen entre sΓ. |
+| **REST** | Representational State Transfer. Estilo de arquitectura para APIs web usando mΓ©todos HTTP (GET, POST, etc.). |
+| **JSON** | JavaScript Object Notation. Formato de texto para intercambiar datos. Ej: `{"nombre": "Juan", "edad": 25}` |
+| **HTTP** | Protocolo de comunicaciΓ³n web. Define cΓ³mo se envΓan y reciben datos en internet. |
+| **Token** | Cadena de texto que identifica a un usuario autenticado. Es como una "llave digital". |
+
+### TΓ©rminos de Frontend
+
+| TΓ©rmino | DefiniciΓ³n |
+|---------|------------|
+| **React** | LibrerΓa de JavaScript para construir interfaces de usuario usando componentes. |
+| **Next.js** | Framework de React que agrega funcionalidades como routing y server-side rendering. |
+| **Componente** | Pieza reutilizable de interfaz. Ej: un botΓ³n, una tabla, un formulario. |
+| **Estado (State)** | Datos que un componente recuerda y que pueden cambiar. Cuando cambian, la UI se actualiza. |
+| **Props** | Datos que un componente padre pasa a un componente hijo. Son inmutables. |
+| **Hook** | Funciones especiales de React (useEffect, useState) que agregan funcionalidad a los componentes. |
+
+### TΓ©rminos de Backend
+
+| TΓ©rmino | DefiniciΓ³n |
+|---------|------------|
+| **Express** | Framework de Node.js para crear servidores web y APIs. |
+| **Middleware** | FunciΓ³n que se ejecuta entre la peticiΓ³n y la respuesta. Puede modificar ambas. |
+| **Router** | Componente que dirige las peticiones a los controladores correctos segΓΊn la URL. |
+| **Controlador** | FunciΓ³n que recibe una peticiΓ³n, la procesa y devuelve una respuesta. |
+| **Servicio** | Capa que contiene la lΓ³gica de negocio y se comunica con la base de datos. |
+
+### TΓ©rminos de Base de Datos
+
+| TΓ©rmino | DefiniciΓ³n |
+|---------|------------|
+| **PostgreSQL** | Sistema de base de datos relacional (usa tablas con filas y columnas). |
+| **Supabase** | Plataforma que proporciona PostgreSQL en la nube con una API automΓ‘tica. |
+| **Query** | Consulta a la base de datos. Ej: "Dame todos los productos con precio > 100". |
+| **JOIN** | OperaciΓ³n que combina datos de mΓΊltiples tablas relacionadas. |
+| **Foreign Key (FK)** | Columna que referencia a otra tabla. Ej: `producto_id` referencia a `productos.id`. |
+
+### TΓ©rminos de DevOps
+
+| TΓ©rmino | DefiniciΓ³n |
+|---------|------------|
+| **Docker** | Herramienta para empaquetar aplicaciones en contenedores aislados. |
+| **Contenedor** | Entorno aislado que contiene todo lo necesario para ejecutar una aplicaciΓ³n. |
+| **Imagen** | Plantilla de solo lectura para crear contenedores. Como un "molde". |
+| **Docker Compose** | Herramienta para definir y ejecutar mΓΊltiples contenedores. |
+| **Nginx** | Servidor web que puede actuar como proxy reverso y balanceador de carga. |
+
+### CΓ³digos de Estado HTTP
+
+| CΓ³digo | Significado | CuΓ‘ndo se usa |
+|--------|-------------|---------------|
+| 200 | OK | La peticiΓ³n fue exitosa |
+| 201 | Created | Se creΓ³ un nuevo recurso exitosamente |
+| 204 | No Content | Γxito pero sin contenido que devolver (ej: DELETE) |
+| 400 | Bad Request | La peticiΓ³n tiene errores (faltan datos, formato incorrecto) |
+| 401 | Unauthorized | No estΓ‘s autenticado (falta token o es invΓ‘lido) |
+| 403 | Forbidden | EstΓ‘s autenticado pero no tienes permiso |
+| 404 | Not Found | El recurso solicitado no existe |
+| 500 | Internal Server Error | Algo fallΓ³ en el servidor |
+
+---
+
+## π Recursos Adicionales
+
+### Para aprender mΓ‘s:
+
+- **JavaScript/React**: [React Documentation](https://react.dev/)
+- **Next.js**: [Next.js Documentation](https://nextjs.org/docs)
+- **Node.js/Express**: [Express Documentation](https://expressjs.com/)
+- **Supabase**: [Supabase Documentation](https://supabase.com/docs)
+- **Docker**: [Docker Getting Started](https://docs.docker.com/get-started/)
+
+---
+
+*DocumentaciΓ³n creada para el TPI de Desarrollo de Software 2025 - FRRe UTN*
diff --git a/api-gateway/nginx.conf b/api-gateway/nginx.conf
index ecde8f9..d3f8619 100644
--- a/api-gateway/nginx.conf
+++ b/api-gateway/nginx.conf
@@ -24,9 +24,9 @@ http {
# β οΈ GRUPO 1 - COMPRAS
# β οΈ NO MODIFICAR ESTE BLOQUE
# ============================================
- # upstream compras-backend {
- # server compras-backend:5000;
- # }
+ upstream compras-backend {
+ server compras-backend:8000;
+ }
# ============================================
# β οΈ GRUPO 2 - STOCK (NOSOTROS)
@@ -40,9 +40,23 @@ http {
# β οΈ GRUPO 3 - LOGΓSTICA
# β οΈ NO MODIFICAR ESTE BLOQUE
# ============================================
- # upstream logistica-backend {
- # server logistica-backend:6000;
- # }
+ upstream logistica-backend {
+ server logistica-backend:3010;
+ }
+
+ # ============================================
+ # KEYCLOAK - Servidor de AutenticaciΓ³n
+ # ============================================
+ upstream keycloak-server {
+ server keycloak:8080;
+ }
+
+ # ============================================
+ # FRONTEND - Next.js
+ # ============================================
+ upstream frontend-server {
+ server stock-frontend:3000;
+ }
server {
listen 80;
@@ -57,29 +71,20 @@ http {
# ============================================
# β οΈ GRUPO 1 - COMPRAS
- # β οΈ DescomentarΓ‘n cuando integren
+ # β οΈ Ahora habilitado para integraciΓ³n
# ============================================
- # location /compras/ {
- # if ($cors_method = 1) {
- # add_header 'Access-Control-Allow-Origin' '*' always;
- # add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
- # add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With' always;
- # add_header 'Access-Control-Max-Age' 1728000;
- # add_header 'Content-Type' 'text/plain; charset=utf-8';
- # add_header 'Content-Length' 0;
- # return 204;
- # }
- #
- # proxy_pass http://compras-backend/;
- # proxy_set_header Host $host;
- # proxy_set_header X-Real-IP $remote_addr;
- # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- # proxy_set_header X-Forwarded-Proto $scheme;
- #
- # add_header 'Access-Control-Allow-Origin' '*' always;
- # add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
- # add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With' always;
- # }
+ location /compras/ {
+ proxy_pass http://compras-backend/;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Prefix /compras;
+ proxy_set_header X-Script-Name /compras;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ }
# ============================================
# β οΈ GRUPO 2 - STOCK (NOSOTROS)
@@ -94,57 +99,51 @@ http {
}
location /stock/ {
- if ($cors_method = 1) {
- add_header 'Access-Control-Allow-Origin' '*' always;
- add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
- add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With' always;
- add_header 'Access-Control-Max-Age' 1728000;
- add_header 'Content-Type' 'text/plain; charset=utf-8';
- add_header 'Content-Length' 0;
- return 204;
- }
-
proxy_pass http://stock-backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
-
- add_header 'Access-Control-Allow-Origin' '*' always;
- add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
- add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With' always;
}
# ============================================
# β οΈ GRUPO 3 - LOGΓSTICA
- # β οΈ DescomentarΓ‘n cuando integren
+ # β οΈ Ahora habilitado para integraciΓ³n
# ============================================
- # location /logistica/ {
- # if ($cors_method = 1) {
- # add_header 'Access-Control-Allow-Origin' '*' always;
- # add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
- # add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With' always;
- # add_header 'Access-Control-Max-Age' 1728000;
- # add_header 'Content-Type' 'text/plain; charset=utf-8';
- # add_header 'Content-Length' 0;
- # return 204;
- # }
- #
- # proxy_pass http://logistica-backend/;
- # proxy_set_header Host $host;
- # proxy_set_header X-Real-IP $remote_addr;
- # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- # proxy_set_header X-Forwarded-Proto $scheme;
- #
- # add_header 'Access-Control-Allow-Origin' '*' always;
- # add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
- # add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With' always;
- # }
-
- # Default location
+ location /logistica/ {
+ proxy_pass http://logistica-backend/;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ # ============================================
+ # KEYCLOAK - Servidor de AutenticaciΓ³n
+ # ============================================
+ location ~ ^/(auth|realms|resources|js|css|fonts|img)/ {
+ proxy_pass http://keycloak-server;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Forwarded-Port $server_port;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ }
+
+ # Default location - Servir frontend
location / {
- return 200 '{"status":"API Gateway Running","endpoints":["/stock/","/compras/","/logistica/"]}';
- add_header Content-Type application/json;
+ proxy_pass http://frontend-server;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
}
}
}
diff --git a/docker-compose.yml b/docker-compose.yml
index 0e5fb79..ba891f0 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,5 +1,3 @@
-version: '3.8'
-
services:
keycloak-db:
image: postgres:15
@@ -38,6 +36,9 @@ services:
networks:
- ds-network
+ # ============================================
+ # GRUPO 2 - STOCK (NOSOTROS)
+ # ============================================
backend:
build:
context: ./mi-app-backend
@@ -53,9 +54,94 @@ services:
- KEYCLOAK_URL=http://keycloak:8080
- KEYCLOAK_REALM=ds-2025-realm
- KEYCLOAK_CLIENT_ID=grupo-02
- - KEYCLOAK_CLIENT_SECRET=grupo-02-secret
+ - KEYCLOAK_CLIENT_SECRET=58536bf8-8501-41c9-b411-786b6d654c25
+ depends_on:
+ - keycloak
+ networks:
+ - ds-network
+
+ # ============================================
+ # GRUPO 1/4 - COMPRAS (GRUPO 04)
+ # ============================================
+ compras-backend:
+ image: ghcr.io/frre-ds/backend-compras-g04:latest
+ container_name: compras-backend
+ ports:
+ - "5000:8000"
+ environment:
+ # ConfiguraciΓ³n del sitio
+ - SITE_URL=http://localhost
+ - APP_PATH_PREFIX=/compras
+ - BASE_API_URL=http://api-gateway/compras/api/
+ # URLs de integraciΓ³n (a travΓ©s del API Gateway)
+ - PRODUCTOS_API_BASE_URL=http://api-gateway/stock
+ - STOCK_API_BASE_URL=http://api-gateway/stock
+ - LOGISTICA_API_BASE_URL=http://api-gateway/logistica
+ # ConfiguraciΓ³n de Keycloak
+ - KEYCLOAK_BASE_URL=http://keycloak:8080
+ - KEYCLOAK_PUBLIC_BASE_URL=http://localhost:8081
+ - KEYCLOAK_REALM=ds-2025-realm
+ - KEYCLOAK_CLIENT_ID=grupo-04
+ - KEYCLOAK_CLIENT_SECRET=6be1bec1-9472-499f-ab37-883d78f57829
depends_on:
- keycloak
+ - backend
+ networks:
+ - ds-network
+
+ # ============================================
+ # GRUPO 3 - LOGΓSTICA (GRUPO 03)
+ # ============================================
+ logistica-mysql:
+ image: mysql:8.0
+ container_name: logistica-mysql
+ restart: always
+ environment:
+ MYSQL_ROOT_PASSWORD: root123
+ MYSQL_DATABASE: shipping_db
+ MYSQL_USER: shipping_user
+ MYSQL_PASSWORD: shipping_pass
+ ports:
+ - "3306:3306"
+ volumes:
+ - logistica_mysql_data:/var/lib/mysql
+ healthcheck:
+ test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "shipping_user", "-pshipping_pass"]
+ interval: 3s
+ timeout: 5s
+ retries: 20
+ start_period: 10s
+ networks:
+ - ds-network
+
+ logistica-backend:
+ image: ghcr.io/frre-ds/2025-grupo-03-backend-logistica:376779cae5a75e077b5a2a324bcedbd10bf93da1
+ container_name: logistica-backend
+ restart: always
+ ports:
+ - "6000:3010"
+ environment:
+ # ConfiguraciΓ³n de MySQL
+ - DB_HOST=logistica-mysql
+ - DB_PORT=3306
+ - DB_USERNAME=shipping_user
+ - DB_PASSWORD=shipping_pass
+ - DB_DATABASE=shipping_db
+ # ConfiguraciΓ³n de Keycloak
+ - KEYCLOAK_URL=http://keycloak:8080
+ - KEYCLOAK_REALM=ds-2025-realm
+ - KEYCLOAK_CLIENT_ID=grupo-03
+ - KEYCLOAK_CLIENT_SECRET=21cd6616-6571-4ee7-be29-0f781f77c74e
+ # URLs de integraciΓ³n
+ - STOCK_API_URL=http://stock-backend:4000
+ - COMPRAS_API_URL=http://compras-backend:5000
+ depends_on:
+ logistica-mysql:
+ condition: service_healthy
+ keycloak:
+ condition: service_started
+ backend:
+ condition: service_started
networks:
- ds-network
@@ -64,7 +150,7 @@ services:
context: ./frontend
dockerfile: Dockerfile
args:
- - NEXT_PUBLIC_API_URL=http://localhost
+ - NEXT_PUBLIC_API_URL=http://localhost/api/v1
- NEXT_PUBLIC_KEYCLOAK_URL=http://localhost:8081
- NEXT_PUBLIC_KEYCLOAK_REALM=ds-2025-realm
- NEXT_PUBLIC_KEYCLOAK_CLIENT_ID=grupo-02
@@ -72,7 +158,7 @@ services:
ports:
- "3000:3000"
environment:
- - NEXT_PUBLIC_API_URL=http://localhost
+ - NEXT_PUBLIC_API_URL=http://localhost/api/v1
- NEXT_PUBLIC_KEYCLOAK_URL=http://localhost:8081
- NEXT_PUBLIC_KEYCLOAK_REALM=ds-2025-realm
- NEXT_PUBLIC_KEYCLOAK_CLIENT_ID=grupo-02
@@ -99,3 +185,4 @@ networks:
volumes:
keycloak_data:
+ logistica_mysql_data:
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index 328e299..73d801a 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -11,7 +11,17 @@ function LayoutContent({ children }: { children: React.ReactNode }) {
const isLoginPage = pathname === "/";
- if (loading) return null;
+ // Mostrar loader mientras carga
+ if (loading) {
+ return (
+
+ );
+ }
// β PROTECCIΓN DE RUTAS: si NO estoy logueado, no entro a nada excepto "/"
if (!authenticated && !isLoginPage) {
diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx
index 66a5cf9..a6ba41d 100644
--- a/frontend/src/app/page.tsx
+++ b/frontend/src/app/page.tsx
@@ -17,10 +17,11 @@ export default function Page() {
const [error, setError] = useState('')
useEffect(() => {
- if (authenticated) {
+ // Solo redirigir si ya terminΓ³ de cargar Y estΓ‘ autenticado
+ if (!loading && authenticated) {
router.push('/dashboard')
}
- }, [authenticated, router])
+ }, [authenticated, loading, router])
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
@@ -81,17 +82,7 @@ export default function Page() {
}
}
- if (loading) {
- return (
-
- )
- }
-
+ // Mostrar la pantalla de login inmediatamente, sin esperar a Keycloak
return (
{/* Lado Izquierdo - Logo y Branding */}
diff --git a/frontend/src/componentes/KeycloakProvider.tsx b/frontend/src/componentes/KeycloakProvider.tsx
index 97f1e8e..631e2da 100644
--- a/frontend/src/componentes/KeycloakProvider.tsx
+++ b/frontend/src/componentes/KeycloakProvider.tsx
@@ -51,7 +51,7 @@ export const KeycloakProvider = ({ children }: { children: ReactNode }) => {
keycloakHasBeenInitialized = true;
keycloak.init({
- onLoad: "check-sso",
+ onLoad: "check-sso", // Verifica SSO sin forzar login
checkLoginIframe: false,
// Tokens solo si NO venimos de un logout
diff --git a/keycloak-client-update.json b/keycloak-client-update.json
new file mode 100644
index 0000000..e4cf0f6
--- /dev/null
+++ b/keycloak-client-update.json
@@ -0,0 +1,11 @@
+{
+ "webOrigins": ["*"],
+ "redirectUris": [
+ "http://localhost/*",
+ "http://localhost:3000/*",
+ "http://localhost/",
+ "http://localhost:3000/"
+ ],
+ "rootUrl": "http://localhost",
+ "baseUrl": "http://localhost"
+}
diff --git a/mi-app-backend/Controladores/imagenesController.js b/mi-app-backend/Controladores/imagenesController.js
new file mode 100644
index 0000000..926668d
--- /dev/null
+++ b/mi-app-backend/Controladores/imagenesController.js
@@ -0,0 +1,39 @@
+// Controlador para informaciΓ³n de imΓ‘genes
+// UbicaciΓ³n: mi-app-backend/Controladores/imagenesController.js
+
+/**
+ * Devuelve informaciΓ³n sobre el repositorio de imΓ‘genes
+ * Γtil para que otros equipos sepan dΓ³nde estΓ‘n las imΓ‘genes
+ */
+const obtenerInfoImagenes = async (req, res) => {
+ try {
+ const info = {
+ repositorio: {
+ owner: process.env.GITHUB_IMAGES_OWNER || 'tu-usuario',
+ repo: process.env.GITHUB_IMAGES_REPO || 'stock-images',
+ branch: process.env.GITHUB_IMAGES_BRANCH || 'main'
+ },
+ baseUrls: {
+ raw: `https://raw.githubusercontent.com/${process.env.GITHUB_IMAGES_OWNER || 'tu-usuario'}/${process.env.GITHUB_IMAGES_REPO || 'stock-images'}/${process.env.GITHUB_IMAGES_BRANCH || 'main'}`,
+ cdn: `https://cdn.jsdelivr.net/gh/${process.env.GITHUB_IMAGES_OWNER || 'tu-usuario'}/${process.env.GITHUB_IMAGES_REPO || 'stock-images'}@${process.env.GITHUB_IMAGES_BRANCH || 'main'}`
+ },
+ formatosAceptados: ['.jpg', '.jpeg', '.png', '.webp', '.gif'],
+ estructura: {
+ productos: '/productos/{producto_id}/{numero}.{ext}',
+ ejemplo: '/productos/1/1.jpg'
+ },
+ notas: [
+ 'Las URLs de productos ya incluyen la URL completa en el campo imagenes[]',
+ 'Se recomienda usar las URLs con CDN (cdn.jsdelivr.net) para mejor rendimiento',
+ 'Solo una imagen por producto puede tener esPrincipal: true'
+ ]
+ };
+
+ res.status(200).json(info);
+ } catch (error) {
+ console.error('Error al obtener info de imΓ‘genes:', error);
+ res.status(500).json({ mensaje: 'Error interno del servidor', error: error.message });
+ }
+};
+
+export default { obtenerInfoImagenes };
diff --git a/mi-app-backend/Rutas/categoriasRoutes.js b/mi-app-backend/Rutas/categoriasRoutes.js
index e09f30f..65a5d1c 100644
--- a/mi-app-backend/Rutas/categoriasRoutes.js
+++ b/mi-app-backend/Rutas/categoriasRoutes.js
@@ -1,25 +1,24 @@
-
-
import express from 'express';
const router = express.Router();
import categoriasControlador from '../Controladores/categoriasController.js';
+import { keycloak } from '../keycloak-config.js';
// --- Definimos todas las rutas de CategorΓas ---
-// GET / (Listar todas)
+// GET / (Listar todas) - PΓΊblico
router.get('/', categoriasControlador.listarCategorias);
-// POST / (Crear una)
-router.post('/', categoriasControlador.crearCategoria);
+// POST / (Crear una) - Protegida
+router.post('/', keycloak.protect(), categoriasControlador.crearCategoria);
-// GET /:categoriaId (Obtener una)
+// GET /:categoriaId (Obtener una) - PΓΊblico
router.get('/:categoriaId', categoriasControlador.obtenerCategoriaPorId);
-// PATCH /:categoriaId (Actualizar una)
-router.patch('/:categoriaId', categoriasControlador.actualizarCategoria);
+// PATCH /:categoriaId (Actualizar una) - Protegida
+router.patch('/:categoriaId', keycloak.protect(), categoriasControlador.actualizarCategoria);
-// DELETE /:categoriaId (Eliminar una)
-router.delete('/:categoriaId', categoriasControlador.eliminarCategoria);
+// DELETE /:categoriaId (Eliminar una) - Protegida
+router.delete('/:categoriaId', keycloak.protect(), categoriasControlador.eliminarCategoria);
// --- Exportamos el router ---
diff --git a/mi-app-backend/Rutas/productosRoutes.js b/mi-app-backend/Rutas/productosRoutes.js
index 50e6b13..2b653db 100644
--- a/mi-app-backend/Rutas/productosRoutes.js
+++ b/mi-app-backend/Rutas/productosRoutes.js
@@ -5,10 +5,14 @@ const router = express.Router();
// 1. Importamos el controlador
import productosControlador from '../Controladores/productosController.js';
+// 2. Importamos keycloak para proteger rutas
+import { keycloak } from '../keycloak-config.js';
+
/*
* =============================
* Ruta: GET /productos
* =============================
+ * PΓΊblico - No requiere autenticaciΓ³n
*/
router.get('/', productosControlador.listarProductos);
@@ -16,6 +20,7 @@ router.get('/', productosControlador.listarProductos);
* =============================
* Ruta: GET /productos/{productoId}
* =============================
+ * PΓΊblico - No requiere autenticaciΓ³n
*/
router.get('/:productoId', productosControlador.obtenerProductoPorId);
@@ -23,22 +28,25 @@ router.get('/:productoId', productosControlador.obtenerProductoPorId);
* =============================
* Ruta: POST /productos
* =============================
+ * Protegida - Requiere autenticaciΓ³n
*/
-router.post('/', productosControlador.crearProducto);
+router.post('/', keycloak.protect(), productosControlador.crearProducto);
/*
* =============================
* Ruta: PATCH /productos/{productoId}
* =============================
+ * Protegida - Requiere autenticaciΓ³n
*/
-router.patch('/:productoId', productosControlador.actualizarProducto);
+router.patch('/:productoId', keycloak.protect(), productosControlador.actualizarProducto);
/*
* =============================
* Ruta: DELETE /productos/{productoId}
* =============================
+ * Protegida - Requiere autenticaciΓ³n
*/
-router.delete('/:productoId', productosControlador.eliminarProducto);
+router.delete('/:productoId', keycloak.protect(), productosControlador.eliminarProducto);
// 3. Exportamos el 'router'
export default router;
\ No newline at end of file
diff --git a/mi-app-backend/Rutas/reservasRoutes.js b/mi-app-backend/Rutas/reservasRoutes.js
index 52a6f5a..64f7611 100644
--- a/mi-app-backend/Rutas/reservasRoutes.js
+++ b/mi-app-backend/Rutas/reservasRoutes.js
@@ -2,11 +2,13 @@
import express from 'express';
const router = express.Router();
import reservasControlador from '../Controladores/reservasController.js';
+import { keycloak } from '../keycloak-config.js';
/*
* =============================
* Ruta: GET /reservas
* =============================
+ * PΓΊblico - No requiere autenticaciΓ³n
*/
router.get('/', reservasControlador.listarReservas);
@@ -14,13 +16,15 @@ router.get('/', reservasControlador.listarReservas);
* =============================
* Ruta: POST /reservas
* =============================
+ * Protegida - Requiere autenticaciΓ³n
*/
-router.post('/', reservasControlador.crearReserva);
+router.post('/', keycloak.protect(), reservasControlador.crearReserva);
/*
* =============================
* Ruta: GET /reservas/{idReserva}
* =============================
+ * PΓΊblico - No requiere autenticaciΓ³n
*/
router.get('/:idReserva', reservasControlador.obtenerReservaPorId);
@@ -28,15 +32,17 @@ router.get('/:idReserva', reservasControlador.obtenerReservaPorId);
* =============================
* Ruta: PATCH /reservas/{idReserva}
* =============================
+ * Protegida - Requiere autenticaciΓ³n
*/
-router.patch('/:idReserva', reservasControlador.actualizarReserva);
+router.patch('/:idReserva', keycloak.protect(), reservasControlador.actualizarReserva);
/*
* =============================
* Ruta: DELETE /reservas/{idReserva}
* =============================
+ * Protegida - Requiere autenticaciΓ³n
*/
-router.delete('/:idReserva', reservasControlador.cancelarReserva);
+router.delete('/:idReserva', keycloak.protect(), reservasControlador.cancelarReserva);
// 3. Exportamos el 'router'
export default router;
\ No newline at end of file
diff --git a/mi-app-backend/Servicios/productosService.js b/mi-app-backend/Servicios/productosService.js
index 3333994..3fdaa7c 100644
--- a/mi-app-backend/Servicios/productosService.js
+++ b/mi-app-backend/Servicios/productosService.js
@@ -1,6 +1,7 @@
-import supabase from '../dbConfig.js';
+import supabase from '../dbConfig.js';
+import { validarImagenes, procesarImagenes } from '../utils/imageHelper.js';
// ===== HELPER DE MAPEO =====
@@ -167,7 +168,18 @@ const crearProducto = async (datosProducto) => {
dimensiones,pesoKg, ubicacion, imagenes
} = datosProducto;
- // 2. Insertar en la tabla principal 'productos'
+ // 1. Validar imΓ‘genes si se proporcionaron
+ if (imagenes && imagenes.length > 0) {
+ const validacion = validarImagenes(imagenes);
+ if (!validacion.valido) {
+ throw new Error(`Error en imΓ‘genes: ${validacion.error}`);
+ }
+ }
+
+ // 2. Procesar imΓ‘genes (convertir a CDN si es necesario)
+ const imagenesProcesadas = imagenes ? procesarImagenes(imagenes) : [];
+
+ // 3. Insertar en la tabla principal 'productos'
const { data: productoData, error: productoError } = await supabase
.from('productos')
.insert({
@@ -178,7 +190,7 @@ const crearProducto = async (datosProducto) => {
dimensiones: dimensiones,
peso_kg: pesoKg,
ubicacion: ubicacion,
- imagenes: imagenes
+ imagenes: imagenesProcesadas
})
.select('id')
.single();
diff --git a/mi-app-backend/index.js b/mi-app-backend/index.js
index 0b64dd2..7d549ec 100644
--- a/mi-app-backend/index.js
+++ b/mi-app-backend/index.js
@@ -11,9 +11,10 @@ const app = express();
const PORT = process.env.PORT || 4000;
const corsOptions = {
- origin: ['http://localhost:3000', 'http://localhost:3001'],
- methods: 'GET,POST,PATCH,DELETE,PUT',
- allowedHeaders: 'Content-Type,Authorization' // <-- IMPORTANTE: AΓ±adir 'Authorization'
+ origin: '*', // Permitir todos los orΓgenes
+ methods: 'GET,POST,PATCH,DELETE,PUT,OPTIONS',
+ allowedHeaders: 'Content-Type,Authorization',
+ credentials: false // Cambiar a false cuando origin es *
};
// --- Middlewares ---
diff --git a/mi-app-backend/utils/imageHelper.js b/mi-app-backend/utils/imageHelper.js
new file mode 100644
index 0000000..252f760
--- /dev/null
+++ b/mi-app-backend/utils/imageHelper.js
@@ -0,0 +1,124 @@
+// Helper para validar y procesar URLs de imΓ‘genes
+// UbicaciΓ³n: mi-app-backend/utils/imageHelper.js
+
+/**
+ * Valida que una URL de imagen sea vΓ‘lida
+ */
+export const validarUrlImagen = (url) => {
+ if (!url || typeof url !== 'string') {
+ return false;
+ }
+
+ // Validar que sea una URL vΓ‘lida
+ try {
+ new URL(url);
+ } catch {
+ return false;
+ }
+
+ // Validar que sea de GitHub o CDN permitido
+ const dominiosPermitidos = [
+ 'raw.githubusercontent.com',
+ 'cdn.jsdelivr.net',
+ 'github.com',
+ 'githubusercontent.com'
+ ];
+
+ const urlObj = new URL(url);
+ const esPermitido = dominiosPermitidos.some(dominio =>
+ urlObj.hostname.includes(dominio)
+ );
+
+ if (!esPermitido) {
+ return false;
+ }
+
+ // Validar extensiΓ³n de archivo
+ const extensionesPermitidas = ['.jpg', '.jpeg', '.png', '.webp', '.gif'];
+ const tieneExtensionValida = extensionesPermitidas.some(ext =>
+ url.toLowerCase().endsWith(ext)
+ );
+
+ return tieneExtensionValida;
+};
+
+/**
+ * Valida un array de objetos de imagen segΓΊn el schema de OpenAPI
+ */
+export const validarImagenes = (imagenes) => {
+ if (!Array.isArray(imagenes)) {
+ return { valido: false, error: 'imagenes debe ser un array' };
+ }
+
+ if (imagenes.length === 0) {
+ return { valido: true }; // Array vacΓo es vΓ‘lido
+ }
+
+ // Verificar que haya exactamente una imagen principal
+ const imagenesPrincipales = imagenes.filter(img => img.esPrincipal === true);
+ if (imagenesPrincipales.length > 1) {
+ return {
+ valido: false,
+ error: 'Solo puede haber una imagen marcada como principal'
+ };
+ }
+
+ // Validar cada imagen
+ for (let i = 0; i < imagenes.length; i++) {
+ const img = imagenes[i];
+
+ // Validar estructura
+ if (!img.url || typeof img.esPrincipal !== 'boolean') {
+ return {
+ valido: false,
+ error: `Imagen ${i}: debe tener 'url' (string) y 'esPrincipal' (boolean)`
+ };
+ }
+
+ // Validar URL
+ if (!validarUrlImagen(img.url)) {
+ return {
+ valido: false,
+ error: `Imagen ${i}: URL invΓ‘lida o no permitida: ${img.url}`
+ };
+ }
+ }
+
+ return { valido: true };
+};
+
+/**
+ * Convierte URL raw de GitHub a URL de CDN (mΓ‘s rΓ‘pida)
+ */
+export const convertirACDN = (url) => {
+ if (!url) return url;
+
+ // Convertir raw.githubusercontent.com a cdn.jsdelivr.net
+ // Ejemplo:
+ // De: https://raw.githubusercontent.com/user/repo/main/image.jpg
+ // A: https://cdn.jsdelivr.net/gh/user/repo@main/image.jpg
+
+ const regex = /^https:\/\/raw\.githubusercontent\.com\/([^\/]+)\/([^\/]+)\/([^\/]+)\/(.+)$/;
+ const match = url.match(regex);
+
+ if (match) {
+ const [, owner, repo, branch, path] = match;
+ return `https://cdn.jsdelivr.net/gh/${owner}/${repo}@${branch}/${path}`;
+ }
+
+ return url; // Si no es raw.githubusercontent.com, devolver original
+};
+
+/**
+ * Procesa array de imΓ‘genes: valida y convierte a CDN
+ */
+export const procesarImagenes = (imagenes) => {
+ if (!imagenes || !Array.isArray(imagenes)) {
+ return [];
+ }
+
+ return imagenes.map(img => ({
+ ...img,
+ url: convertirACDN(img.url)
+ }));
+};
diff --git a/package-lock.json b/package-lock.json
index f85dedc..a7da6b8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2559,23 +2559,27 @@
"license": "MIT"
},
"node_modules/body-parser": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
- "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
+ "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==",
"license": "MIT",
"dependencies": {
"bytes": "^3.1.2",
"content-type": "^1.0.5",
- "debug": "^4.4.0",
+ "debug": "^4.4.3",
"http-errors": "^2.0.0",
- "iconv-lite": "^0.6.3",
+ "iconv-lite": "^0.7.0",
"on-finished": "^2.4.1",
"qs": "^6.14.0",
- "raw-body": "^3.0.0",
- "type-is": "^2.0.0"
+ "raw-body": "^3.0.1",
+ "type-is": "^2.0.1"
},
"engines": {
"node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/brace-expansion": {
@@ -4858,15 +4862,19 @@
}
},
"node_modules/iconv-lite": {
- "version": "0.6.3",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
- "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
+ "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/ignore": {
@@ -6940,22 +6948,6 @@
"node": ">= 0.10"
}
},
- "node_modules/raw-body/node_modules/iconv-lite": {
- "version": "0.7.0",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
- "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
- "license": "MIT",
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
"node_modules/react": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
diff --git a/scripts/upload-images.js b/scripts/upload-images.js
new file mode 100644
index 0000000..9008f80
--- /dev/null
+++ b/scripts/upload-images.js
@@ -0,0 +1,73 @@
+// Script para subir imΓ‘genes a GitHub y generar URLs
+// Usar con: node scripts/upload-images.js
+
+import { Octokit } from '@octokit/rest';
+import fs from 'fs';
+import path from 'path';
+
+const GITHUB_TOKEN = process.env.GITHUB_TOKEN; // Personal Access Token
+const OWNER = 'tu-usuario'; // Tu usuario de GitHub
+const REPO = 'stock-images'; // Nombre del repo de imΓ‘genes
+const BRANCH = 'main';
+
+const octokit = new Octokit({ auth: GITHUB_TOKEN });
+
+/**
+ * Sube una imagen a GitHub y devuelve la URL raw
+ */
+async function uploadImage(localPath, remotePath) {
+ try {
+ const content = fs.readFileSync(localPath, { encoding: 'base64' });
+
+ const response = await octokit.repos.createOrUpdateFileContents({
+ owner: OWNER,
+ repo: REPO,
+ path: remotePath,
+ message: `Add product image: ${path.basename(remotePath)}`,
+ content: content,
+ branch: BRANCH
+ });
+
+ // URL raw de GitHub
+ const rawUrl = `https://raw.githubusercontent.com/${OWNER}/${REPO}/${BRANCH}/${remotePath}`;
+
+ // URL con CDN (jsDelivr - mΓ‘s rΓ‘pido)
+ const cdnUrl = `https://cdn.jsdelivr.net/gh/${OWNER}/${REPO}@${BRANCH}/${remotePath}`;
+
+ console.log(`β
Imagen subida: ${remotePath}`);
+ console.log(` Raw URL: ${rawUrl}`);
+ console.log(` CDN URL: ${cdnUrl}`);
+
+ return { rawUrl, cdnUrl };
+ } catch (error) {
+ console.error(`β Error subiendo ${remotePath}:`, error.message);
+ throw error;
+ }
+}
+
+/**
+ * Ejemplo de uso: Subir imΓ‘genes de un producto
+ */
+async function uploadProductImages(productId, imageFiles) {
+ const urls = [];
+
+ for (let i = 0; i < imageFiles.length; i++) {
+ const localPath = imageFiles[i];
+ const ext = path.extname(localPath);
+ const remotePath = `productos/${productId}/${i + 1}${ext}`;
+
+ const { cdnUrl } = await uploadImage(localPath, remotePath);
+ urls.push({
+ url: cdnUrl,
+ esPrincipal: i === 0 // La primera imagen es principal
+ });
+ }
+
+ return urls;
+}
+
+// Ejemplo de uso
+// uploadProductImages(1, ['./temp/laptop-front.jpg', './temp/laptop-side.jpg'])
+// .then(urls => console.log('URLs generadas:', JSON.stringify(urls, null, 2)));
+
+export { uploadImage, uploadProductImages };