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 ( +
+
+ setEmail(e.target.value)} /> + setPassword(e.target.value)} /> + +
+
+ ) +} +``` + +**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 ( + + + + + + + + + + {/* .map() recorre cada producto y crea una fila */} + {productos.map((producto) => ( + + + + + + ))} + +
IDNombrePrecio
{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 ( +
+
+
+

Cargando...

+
+
+ ); + } // ⭐ 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 ( -
-
-
-

Cargando...

-
-
- ) - } - + // 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 };