├── .prettierrc ├── tsconfig.build.json ├── src ├── app.service.ts ├── main.ts ├── app.module.ts ├── app.controller.ts └── app.controller.spec.ts ├── nest-cli.json ├── microservices ├── api-gateway │ ├── nest-cli.json │ ├── src │ │ ├── modules │ │ │ ├── auth │ │ │ │ ├── jwt-auth.guard.ts │ │ │ │ ├── jwt.strategy.ts │ │ │ │ ├── auth.module.ts │ │ │ │ ├── auth.controller.ts │ │ │ │ └── auth.service.ts │ │ │ ├── user │ │ │ │ ├── user.module.ts │ │ │ │ ├── user.controller.ts │ │ │ │ └── user.service.ts │ │ │ ├── product │ │ │ │ ├── product.module.ts │ │ │ │ ├── product.controller.ts │ │ │ │ └── product.service.ts │ │ │ └── notification │ │ │ │ ├── notification.module.ts │ │ │ │ ├── notification.controller.ts │ │ │ │ └── notification.service.ts │ │ ├── app.controller.ts │ │ ├── app.service.ts │ │ ├── main.ts │ │ └── app.module.ts │ ├── Dockerfile │ ├── tsconfig.json │ └── package.json ├── user-service │ ├── nest-cli.json │ ├── src │ │ ├── modules │ │ │ ├── auth │ │ │ │ ├── jwt-auth.guard.ts │ │ │ │ ├── jwt.strategy.ts │ │ │ │ ├── auth.module.ts │ │ │ │ ├── auth.controller.ts │ │ │ │ └── auth.service.ts │ │ │ └── user │ │ │ │ ├── dto │ │ │ │ ├── update-user.dto.ts │ │ │ │ ├── login.dto.ts │ │ │ │ └── create-user.dto.ts │ │ │ │ ├── user.module.ts │ │ │ │ ├── entities │ │ │ │ └── user.entity.ts │ │ │ │ ├── user.controller.ts │ │ │ │ └── user.service.ts │ │ ├── app.controller.ts │ │ ├── app.service.ts │ │ ├── main.ts │ │ └── app.module.ts │ ├── Dockerfile │ ├── tsconfig.json │ └── package.json ├── product-service │ ├── nest-cli.json │ ├── src │ │ ├── modules │ │ │ ├── product │ │ │ │ ├── dto │ │ │ │ │ ├── update-product.dto.ts │ │ │ │ │ ├── add-review.dto.ts │ │ │ │ │ └── create-product.dto.ts │ │ │ │ ├── product.module.ts │ │ │ │ ├── schemas │ │ │ │ │ └── product.schema.ts │ │ │ │ ├── product.controller.ts │ │ │ │ └── product.service.ts │ │ │ └── category │ │ │ │ ├── dto │ │ │ │ ├── update-category.dto.ts │ │ │ │ └── create-category.dto.ts │ │ │ │ ├── category.module.ts │ │ │ │ ├── schemas │ │ │ │ └── category.schema.ts │ │ │ │ ├── category.controller.ts │ │ │ │ └── category.service.ts │ │ ├── app.controller.ts │ │ ├── app.service.ts │ │ ├── main.ts │ │ └── app.module.ts │ ├── Dockerfile │ ├── tsconfig.json │ └── package.json └── notification-service │ ├── nest-cli.json │ ├── src │ ├── modules │ │ ├── redis │ │ │ ├── redis.module.ts │ │ │ └── redis.service.ts │ │ ├── websocket │ │ │ ├── websocket.module.ts │ │ │ └── notifications.gateway.ts │ │ └── notification │ │ │ ├── notification.module.ts │ │ │ ├── interfaces │ │ │ └── notification.interface.ts │ │ │ ├── dto │ │ │ └── send-notification.dto.ts │ │ │ ├── notification.controller.ts │ │ │ └── notification.service.ts │ ├── app.controller.ts │ ├── app.service.ts │ ├── app.module.ts │ └── main.ts │ ├── Dockerfile │ ├── tsconfig.json │ └── package.json ├── test ├── jest-e2e.json └── app.e2e-spec.ts ├── scripts ├── stop-dev.sh ├── build-all.sh ├── start-dev.sh ├── quick-test.sh └── test-all.sh ├── shared ├── common │ ├── constants │ │ └── service-urls.ts │ └── interfaces │ │ └── api-response.interface.ts └── config │ └── database.config.ts ├── tsconfig.json ├── eslint.config.mjs ├── env.example ├── package.json ├── .gitignore ├── docker-compose.yml ├── README.md └── test-endpoints.md /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /microservices/api-gateway/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /microservices/api-gateway/src/modules/auth/jwt-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class JwtAuthGuard extends AuthGuard('jwt') {} 6 | -------------------------------------------------------------------------------- /microservices/user-service/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /microservices/user-service/src/modules/auth/jwt-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class JwtAuthGuard extends AuthGuard('jwt') {} 6 | -------------------------------------------------------------------------------- /microservices/product-service/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /microservices/notification-service/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /microservices/product-service/src/modules/product/dto/update-product.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/mapped-types'; 2 | import { CreateProductDto } from './create-product.dto'; 3 | 4 | export class UpdateProductDto extends PartialType(CreateProductDto) {} 5 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | await app.listen(process.env.PORT ?? 3000); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /microservices/product-service/src/modules/category/dto/update-category.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/mapped-types'; 2 | import { CreateCategoryDto } from './create-category.dto'; 3 | 4 | export class UpdateCategoryDto extends PartialType(CreateCategoryDto) {} 5 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | @Module({ 6 | imports: [], 7 | controllers: [AppController], 8 | providers: [AppService], 9 | }) 10 | export class AppModule {} 11 | -------------------------------------------------------------------------------- /microservices/notification-service/src/modules/redis/redis.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, Global } from '@nestjs/common'; 2 | import { RedisService } from './redis.service'; 3 | 4 | @Global() 5 | @Module({ 6 | providers: [RedisService], 7 | exports: [RedisService], 8 | }) 9 | export class RedisModule {} 10 | -------------------------------------------------------------------------------- /microservices/notification-service/src/modules/websocket/websocket.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { NotificationsGateway } from './notifications.gateway'; 3 | 4 | @Module({ 5 | providers: [NotificationsGateway], 6 | exports: [NotificationsGateway], 7 | }) 8 | export class WebsocketModule {} 9 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /microservices/user-service/src/modules/user/dto/update-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/mapped-types'; 2 | import { IsOptional, IsBoolean } from 'class-validator'; 3 | import { CreateUserDto } from './create-user.dto'; 4 | 5 | export class UpdateUserDto extends PartialType(CreateUserDto) { 6 | @IsOptional() 7 | @IsBoolean({ message: 'isActive debe ser un valor booleano' }) 8 | isActive?: boolean; 9 | } 10 | -------------------------------------------------------------------------------- /microservices/api-gateway/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | 3 | WORKDIR /app 4 | 5 | # Copiar archivos de dependencias 6 | COPY package*.json ./ 7 | 8 | # Instalar dependencias 9 | RUN npm install --legacy-peer-deps 10 | 11 | # Copiar código fuente 12 | COPY . . 13 | 14 | # Compilar la aplicación 15 | RUN npm run build 16 | 17 | # Exponer puerto 18 | EXPOSE 3000 19 | 20 | # Comando por defecto 21 | CMD ["npm", "run", "start:prod"] 22 | -------------------------------------------------------------------------------- /microservices/api-gateway/src/modules/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { HttpModule } from '@nestjs/axios'; 3 | import { UserController } from './user.controller'; 4 | import { UserService } from './user.service'; 5 | 6 | @Module({ 7 | imports: [HttpModule], 8 | controllers: [UserController], 9 | providers: [UserService], 10 | exports: [UserService], 11 | }) 12 | export class UserModule {} 13 | -------------------------------------------------------------------------------- /microservices/user-service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | 3 | WORKDIR /app 4 | 5 | # Copiar archivos de dependencias 6 | COPY package*.json ./ 7 | 8 | # Instalar dependencias 9 | RUN npm install --legacy-peer-deps 10 | 11 | # Copiar código fuente 12 | COPY . . 13 | 14 | # Compilar la aplicación 15 | RUN npm run build 16 | 17 | # Exponer puerto 18 | EXPOSE 3001 19 | 20 | # Comando por defecto 21 | CMD ["npm", "run", "start:prod"] 22 | -------------------------------------------------------------------------------- /microservices/product-service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | 3 | WORKDIR /app 4 | 5 | # Copiar archivos de dependencias 6 | COPY package*.json ./ 7 | 8 | # Instalar dependencias 9 | RUN npm install --legacy-peer-deps 10 | 11 | # Copiar código fuente 12 | COPY . . 13 | 14 | # Compilar la aplicación 15 | RUN npm run build 16 | 17 | # Exponer puerto 18 | EXPOSE 3002 19 | 20 | # Comando por defecto 21 | CMD ["npm", "run", "start:prod"] 22 | -------------------------------------------------------------------------------- /microservices/notification-service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | 3 | WORKDIR /app 4 | 5 | # Copiar archivos de dependencias 6 | COPY package*.json ./ 7 | 8 | # Instalar dependencias 9 | RUN npm install --legacy-peer-deps 10 | 11 | # Copiar código fuente 12 | COPY . . 13 | 14 | # Compilar la aplicación 15 | RUN npm run build 16 | 17 | # Exponer puerto 18 | EXPOSE 3003 19 | 20 | # Comando por defecto 21 | CMD ["npm", "run", "start:prod"] 22 | -------------------------------------------------------------------------------- /microservices/api-gateway/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getApiInfo() { 10 | return this.appService.getApiInfo(); 11 | } 12 | 13 | @Get('health') 14 | getHealth() { 15 | return this.appService.getHealth(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /microservices/api-gateway/src/modules/product/product.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { HttpModule } from '@nestjs/axios'; 3 | import { ProductController } from './product.controller'; 4 | import { ProductService } from './product.service'; 5 | 6 | @Module({ 7 | imports: [HttpModule], 8 | controllers: [ProductController], 9 | providers: [ProductService], 10 | exports: [ProductService], 11 | }) 12 | export class ProductModule {} 13 | -------------------------------------------------------------------------------- /microservices/product-service/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getServiceInfo() { 10 | return this.appService.getServiceInfo(); 11 | } 12 | 13 | @Get('health') 14 | getHealth() { 15 | return this.appService.getHealth(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /microservices/user-service/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getServiceInfo() { 10 | return this.appService.getServiceInfo(); 11 | } 12 | 13 | @Get('health') 14 | getHealth() { 15 | return this.appService.getHealth(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /microservices/notification-service/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getServiceInfo() { 10 | return this.appService.getServiceInfo(); 11 | } 12 | 13 | @Get('health') 14 | getHealth() { 15 | return this.appService.getHealth(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /microservices/user-service/src/modules/user/dto/login.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; 2 | 3 | export class LoginDto { 4 | @IsEmail({}, { message: 'El email debe tener un formato válido' }) 5 | @IsNotEmpty({ message: 'El email es requerido' }) 6 | email: string; 7 | 8 | @IsString({ message: 'La contraseña debe ser una cadena de texto' }) 9 | @IsNotEmpty({ message: 'La contraseña es requerida' }) 10 | password: string; 11 | } 12 | -------------------------------------------------------------------------------- /scripts/stop-dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script para detener todos los servicios 4 | 5 | echo "🛑 Deteniendo microservicios..." 6 | 7 | docker-compose down 8 | 9 | echo "✅ Todos los servicios han sido detenidos." 10 | 11 | # Preguntar si quiere limpiar volúmenes 12 | read -p "¿Deseas eliminar también los volúmenes de datos? (y/N): " -n 1 -r 13 | echo 14 | if [[ $REPLY =~ ^[Yy]$ ]]; then 15 | echo "🗑️ Eliminando volúmenes..." 16 | docker-compose down -v 17 | echo "✅ Volúmenes eliminados." 18 | fi 19 | -------------------------------------------------------------------------------- /microservices/api-gateway/src/modules/notification/notification.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { HttpModule } from '@nestjs/axios'; 3 | import { NotificationController } from './notification.controller'; 4 | import { NotificationService } from './notification.service'; 5 | 6 | @Module({ 7 | imports: [HttpModule], 8 | controllers: [NotificationController], 9 | providers: [NotificationService], 10 | exports: [NotificationService], 11 | }) 12 | export class NotificationModule {} 13 | -------------------------------------------------------------------------------- /microservices/user-service/src/modules/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { UserController } from './user.controller'; 4 | import { UserService } from './user.service'; 5 | import { User } from './entities/user.entity'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([User])], 9 | controllers: [UserController], 10 | providers: [UserService], 11 | exports: [UserService, TypeOrmModule], 12 | }) 13 | export class UserModule {} 14 | -------------------------------------------------------------------------------- /microservices/notification-service/src/modules/notification/notification.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { NotificationController } from './notification.controller'; 3 | import { NotificationService } from './notification.service'; 4 | import { WebsocketModule } from '../websocket/websocket.module'; 5 | 6 | @Module({ 7 | imports: [WebsocketModule], 8 | controllers: [NotificationController], 9 | providers: [NotificationService], 10 | exports: [NotificationService], 11 | }) 12 | export class NotificationModule {} 13 | -------------------------------------------------------------------------------- /shared/common/constants/service-urls.ts: -------------------------------------------------------------------------------- 1 | export const SERVICE_URLS = { 2 | API_GATEWAY: process.env.API_GATEWAY_URL || 'http://localhost:3000', 3 | USER_SERVICE: process.env.USER_SERVICE_URL || 'http://localhost:3001', 4 | PRODUCT_SERVICE: process.env.PRODUCT_SERVICE_URL || 'http://localhost:3002', 5 | NOTIFICATION_SERVICE: process.env.NOTIFICATION_SERVICE_URL || 'http://localhost:3003', 6 | } as const; 7 | 8 | export const SERVICE_PORTS = { 9 | API_GATEWAY: 3000, 10 | USER_SERVICE: 3001, 11 | PRODUCT_SERVICE: 3002, 12 | NOTIFICATION_SERVICE: 3003, 13 | } as const; 14 | -------------------------------------------------------------------------------- /shared/common/interfaces/api-response.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ApiResponse { 2 | success: boolean; 3 | message?: string; 4 | data?: T; 5 | error?: string; 6 | statusCode?: number; 7 | timestamp?: string; 8 | } 9 | 10 | export interface PaginatedResponse { 11 | data: T[]; 12 | total: number; 13 | page: number; 14 | limit: number; 15 | totalPages: number; 16 | } 17 | 18 | export interface ServiceHealth { 19 | status: 'ok' | 'error'; 20 | service: string; 21 | timestamp: string; 22 | uptime: number; 23 | database?: string; 24 | websockets?: string; 25 | redis?: string; 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2023", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "noImplicitAny": false, 18 | "strictBindCallApply": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /microservices/product-service/src/modules/category/category.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | import { CategoryController } from './category.controller'; 4 | import { CategoryService } from './category.service'; 5 | import { Category, CategorySchema } from './schemas/category.schema'; 6 | 7 | @Module({ 8 | imports: [ 9 | MongooseModule.forFeature([{ name: Category.name, schema: CategorySchema }]), 10 | ], 11 | controllers: [CategoryController], 12 | providers: [CategoryService], 13 | exports: [CategoryService, MongooseModule], 14 | }) 15 | export class CategoryModule {} 16 | -------------------------------------------------------------------------------- /microservices/api-gateway/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2023", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "noImplicitAny": false, 18 | "strictBindCallApply": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /microservices/product-service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2023", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "noImplicitAny": false, 18 | "strictBindCallApply": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /microservices/user-service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2023", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "noImplicitAny": false, 18 | "strictBindCallApply": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /microservices/notification-service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2023", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "noImplicitAny": false, 18 | "strictBindCallApply": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /microservices/product-service/src/modules/product/product.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | import { ProductController } from './product.controller'; 4 | import { ProductService } from './product.service'; 5 | import { Product, ProductSchema } from './schemas/product.schema'; 6 | import { CategoryModule } from '../category/category.module'; 7 | 8 | @Module({ 9 | imports: [ 10 | MongooseModule.forFeature([{ name: Product.name, schema: ProductSchema }]), 11 | CategoryModule, 12 | ], 13 | controllers: [ProductController], 14 | providers: [ProductService], 15 | exports: [ProductService], 16 | }) 17 | export class ProductModule {} 18 | -------------------------------------------------------------------------------- /microservices/api-gateway/src/modules/auth/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable } from '@nestjs/common'; 4 | 5 | @Injectable() 6 | export class JwtStrategy extends PassportStrategy(Strategy) { 7 | constructor() { 8 | super({ 9 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 10 | ignoreExpiration: false, 11 | secretOrKey: process.env.JWT_SECRET || 'your-super-secret-jwt-key', 12 | }); 13 | } 14 | 15 | async validate(payload: any) { 16 | return { 17 | id: payload.sub, 18 | email: payload.email, 19 | roles: payload.roles, 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /microservices/user-service/src/modules/auth/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable } from '@nestjs/common'; 4 | 5 | @Injectable() 6 | export class JwtStrategy extends PassportStrategy(Strategy) { 7 | constructor() { 8 | super({ 9 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 10 | ignoreExpiration: false, 11 | secretOrKey: process.env.JWT_SECRET || 'your-super-secret-jwt-key', 12 | }); 13 | } 14 | 15 | async validate(payload: any) { 16 | return { 17 | id: payload.sub, 18 | email: payload.email, 19 | roles: payload.roles, 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /scripts/build-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script para construir todos los microservicios 4 | 5 | echo "🔨 Construyendo todos los microservicios..." 6 | 7 | services=("api-gateway" "user-service" "product-service" "notification-service") 8 | 9 | for service in "${services[@]}"; do 10 | echo "📦 Construyendo $service..." 11 | 12 | cd "microservices/$service" 13 | 14 | if [ -f "package.json" ]; then 15 | npm install 16 | npm run build 17 | echo "✅ $service construido exitosamente" 18 | else 19 | echo "❌ No se encontró package.json en $service" 20 | fi 21 | 22 | cd "../.." 23 | done 24 | 25 | echo "🎉 Construcción completada para todos los servicios!" 26 | -------------------------------------------------------------------------------- /src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /microservices/product-service/src/modules/category/schemas/category.schema.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | import { Document } from 'mongoose'; 3 | 4 | export type CategoryDocument = Category & Document; 5 | 6 | @Schema({ timestamps: true }) 7 | export class Category { 8 | @Prop({ required: true, unique: true }) 9 | name: string; 10 | 11 | @Prop({ required: true }) 12 | description: string; 13 | 14 | @Prop({ required: true, unique: true }) 15 | slug: string; 16 | 17 | @Prop({ default: true }) 18 | isActive: boolean; 19 | 20 | @Prop() 21 | image?: string; 22 | 23 | @Prop({ type: Object }) 24 | metadata?: Record; 25 | } 26 | 27 | export const CategorySchema = SchemaFactory.createForClass(Category); 28 | -------------------------------------------------------------------------------- /microservices/user-service/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getServiceInfo() { 6 | return { 7 | name: 'User Service', 8 | version: '1.0.0', 9 | description: 'Microservicio para gestión de usuarios', 10 | database: 'PostgreSQL', 11 | endpoints: { 12 | users: '/users', 13 | auth: '/auth', 14 | }, 15 | status: 'running', 16 | timestamp: new Date().toISOString(), 17 | }; 18 | } 19 | 20 | getHealth() { 21 | return { 22 | status: 'ok', 23 | service: 'user-service', 24 | timestamp: new Date().toISOString(), 25 | uptime: process.uptime(), 26 | database: 'connected', 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { App } from 'supertest/types'; 5 | import { AppModule } from './../src/app.module'; 6 | 7 | describe('AppController (e2e)', () => { 8 | let app: INestApplication; 9 | 10 | beforeEach(async () => { 11 | const moduleFixture: TestingModule = await Test.createTestingModule({ 12 | imports: [AppModule], 13 | }).compile(); 14 | 15 | app = moduleFixture.createNestApplication(); 16 | await app.init(); 17 | }); 18 | 19 | it('/ (GET)', () => { 20 | return request(app.getHttpServer()) 21 | .get('/') 22 | .expect(200) 23 | .expect('Hello World!'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /microservices/api-gateway/src/modules/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { HttpModule } from '@nestjs/axios'; 3 | import { JwtModule } from '@nestjs/jwt'; 4 | import { PassportModule } from '@nestjs/passport'; 5 | import { AuthController } from './auth.controller'; 6 | import { AuthService } from './auth.service'; 7 | import { JwtStrategy } from './jwt.strategy'; 8 | 9 | @Module({ 10 | imports: [ 11 | HttpModule, 12 | PassportModule, 13 | JwtModule.register({ 14 | secret: process.env.JWT_SECRET || 'your-super-secret-jwt-key', 15 | signOptions: { expiresIn: '24h' }, 16 | }), 17 | ], 18 | controllers: [AuthController], 19 | providers: [AuthService, JwtStrategy], 20 | exports: [AuthService], 21 | }) 22 | export class AuthModule {} 23 | -------------------------------------------------------------------------------- /microservices/product-service/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getServiceInfo() { 6 | return { 7 | name: 'Product Service', 8 | version: '1.0.0', 9 | description: 'Microservicio para gestión de productos', 10 | database: 'MongoDB', 11 | endpoints: { 12 | products: '/products', 13 | categories: '/categories', 14 | }, 15 | status: 'running', 16 | timestamp: new Date().toISOString(), 17 | }; 18 | } 19 | 20 | getHealth() { 21 | return { 22 | status: 'ok', 23 | service: 'product-service', 24 | timestamp: new Date().toISOString(), 25 | uptime: process.uptime(), 26 | database: 'connected', 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /microservices/user-service/src/modules/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { JwtModule } from '@nestjs/jwt'; 3 | import { PassportModule } from '@nestjs/passport'; 4 | import { AuthController } from './auth.controller'; 5 | import { AuthService } from './auth.service'; 6 | import { UserModule } from '../user/user.module'; 7 | import { JwtStrategy } from './jwt.strategy'; 8 | 9 | @Module({ 10 | imports: [ 11 | UserModule, 12 | PassportModule, 13 | JwtModule.register({ 14 | secret: process.env.JWT_SECRET || 'your-super-secret-jwt-key', 15 | signOptions: { expiresIn: '24h' }, 16 | }), 17 | ], 18 | controllers: [AuthController], 19 | providers: [AuthService, JwtStrategy], 20 | exports: [AuthService], 21 | }) 22 | export class AuthModule {} 23 | -------------------------------------------------------------------------------- /microservices/product-service/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { ValidationPipe } from '@nestjs/common'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule); 7 | 8 | // Habilitar CORS 9 | app.enableCors({ 10 | origin: true, 11 | credentials: true, 12 | }); 13 | 14 | // Configurar validación global 15 | app.useGlobalPipes(new ValidationPipe({ 16 | transform: true, 17 | whitelist: true, 18 | forbidNonWhitelisted: true, 19 | })); 20 | 21 | const port = process.env.PORT || 3002; 22 | await app.listen(port); 23 | 24 | console.log(`📦 Product Service ejecutándose en puerto ${port}`); 25 | console.log(`🍃 Base de datos MongoDB: ${process.env.MONGODB_URI}`); 26 | } 27 | 28 | bootstrap(); 29 | -------------------------------------------------------------------------------- /microservices/api-gateway/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getApiInfo() { 6 | return { 7 | name: 'Microservices API Gateway', 8 | version: '1.0.0', 9 | description: 'Gateway para acceder a todos los microservicios', 10 | endpoints: { 11 | users: '/api/v1/users', 12 | products: '/api/v1/products', 13 | notifications: '/api/v1/notifications', 14 | auth: '/api/v1/auth', 15 | }, 16 | status: 'running', 17 | timestamp: new Date().toISOString(), 18 | }; 19 | } 20 | 21 | getHealth() { 22 | return { 23 | status: 'ok', 24 | timestamp: new Date().toISOString(), 25 | uptime: process.uptime(), 26 | environment: process.env.NODE_ENV || 'development', 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /microservices/notification-service/src/modules/notification/interfaces/notification.interface.ts: -------------------------------------------------------------------------------- 1 | export interface INotification { 2 | id?: string; 3 | userId: string; 4 | title: string; 5 | message: string; 6 | type: NotificationType; 7 | data?: Record; 8 | read?: boolean; 9 | timestamp?: string; 10 | } 11 | 12 | export enum NotificationType { 13 | INFO = 'info', 14 | SUCCESS = 'success', 15 | WARNING = 'warning', 16 | ERROR = 'error', 17 | USER_ACTION = 'user_action', 18 | SYSTEM = 'system', 19 | PRODUCT_UPDATE = 'product_update', 20 | ORDER_STATUS = 'order_status', 21 | } 22 | 23 | export interface ISendNotificationDto { 24 | userId?: string; 25 | userIds?: string[]; 26 | title: string; 27 | message: string; 28 | type: NotificationType; 29 | data?: Record; 30 | broadcast?: boolean; 31 | } 32 | -------------------------------------------------------------------------------- /microservices/user-service/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { ValidationPipe } from '@nestjs/common'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule); 7 | 8 | // Habilitar CORS 9 | app.enableCors({ 10 | origin: true, 11 | credentials: true, 12 | }); 13 | 14 | // Configurar validación global 15 | app.useGlobalPipes(new ValidationPipe({ 16 | transform: true, 17 | whitelist: true, 18 | forbidNonWhitelisted: true, 19 | })); 20 | 21 | const port = process.env.PORT || 3001; 22 | await app.listen(port); 23 | 24 | console.log(`🔐 User Service ejecutándose en puerto ${port}`); 25 | console.log(`🗄️ Base de datos: ${process.env.DATABASE_HOST}:${process.env.DATABASE_PORT}/${process.env.DATABASE_NAME}`); 26 | } 27 | 28 | bootstrap(); 29 | -------------------------------------------------------------------------------- /microservices/notification-service/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getServiceInfo() { 6 | return { 7 | name: 'Notification Service', 8 | version: '1.0.0', 9 | description: 'Microservicio para notificaciones en tiempo real', 10 | features: ['WebSockets', 'Redis', 'Real-time notifications'], 11 | endpoints: { 12 | notifications: '/notifications', 13 | websocket: '/socket.io/', 14 | }, 15 | status: 'running', 16 | timestamp: new Date().toISOString(), 17 | }; 18 | } 19 | 20 | getHealth() { 21 | return { 22 | status: 'ok', 23 | service: 'notification-service', 24 | timestamp: new Date().toISOString(), 25 | uptime: process.uptime(), 26 | websockets: 'active', 27 | redis: 'connected', 28 | }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /microservices/notification-service/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { JwtModule } from '@nestjs/jwt'; 3 | import { PassportModule } from '@nestjs/passport'; 4 | 5 | import { AppController } from './app.controller'; 6 | import { AppService } from './app.service'; 7 | import { NotificationModule } from './modules/notification/notification.module'; 8 | import { WebsocketModule } from './modules/websocket/websocket.module'; 9 | import { RedisModule } from './modules/redis/redis.module'; 10 | 11 | @Module({ 12 | imports: [ 13 | PassportModule, 14 | JwtModule.register({ 15 | secret: process.env.JWT_SECRET || 'your-super-secret-jwt-key', 16 | signOptions: { expiresIn: '24h' }, 17 | }), 18 | RedisModule, 19 | NotificationModule, 20 | WebsocketModule, 21 | ], 22 | controllers: [AppController], 23 | providers: [AppService], 24 | }) 25 | export class AppModule {} 26 | -------------------------------------------------------------------------------- /microservices/user-service/src/modules/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import { CreateUserDto } from '../user/dto/create-user.dto'; 4 | import { LoginDto } from '../user/dto/login.dto'; 5 | 6 | @Controller('auth') 7 | export class AuthController { 8 | constructor(private readonly authService: AuthService) {} 9 | 10 | @Post('register') 11 | async register(@Body() createUserDto: CreateUserDto) { 12 | return this.authService.register(createUserDto); 13 | } 14 | 15 | @Post('login') 16 | @HttpCode(HttpStatus.OK) 17 | async login(@Body() loginDto: LoginDto) { 18 | return this.authService.login(loginDto); 19 | } 20 | 21 | @Post('validate-token') 22 | @HttpCode(HttpStatus.OK) 23 | async validateToken(@Body('token') token: string) { 24 | return this.authService.validateToken(token); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /microservices/notification-service/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { ValidationPipe } from '@nestjs/common'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule); 7 | 8 | // Habilitar CORS para WebSockets 9 | app.enableCors({ 10 | origin: true, 11 | credentials: true, 12 | }); 13 | 14 | // Configurar validación global 15 | app.useGlobalPipes(new ValidationPipe({ 16 | transform: true, 17 | whitelist: true, 18 | forbidNonWhitelisted: true, 19 | })); 20 | 21 | const port = process.env.PORT || 3003; 22 | await app.listen(port); 23 | 24 | console.log(`🔔 Notification Service ejecutándose en puerto ${port}`); 25 | console.log(`📡 WebSockets disponibles en ws://localhost:${port}`); 26 | console.log(`🔴 Redis: ${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`); 27 | } 28 | 29 | bootstrap(); 30 | -------------------------------------------------------------------------------- /microservices/product-service/src/modules/product/dto/add-review.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString, IsNumber, Min, Max } from 'class-validator'; 2 | 3 | export class AddReviewDto { 4 | @IsNotEmpty({ message: 'El ID de usuario es requerido' }) 5 | @IsString({ message: 'El ID de usuario debe ser una cadena de texto' }) 6 | userId: string; 7 | 8 | @IsNotEmpty({ message: 'El nombre de usuario es requerido' }) 9 | @IsString({ message: 'El nombre de usuario debe ser una cadena de texto' }) 10 | userName: string; 11 | 12 | @IsNotEmpty({ message: 'La calificación es requerida' }) 13 | @IsNumber({}, { message: 'La calificación debe ser un número' }) 14 | @Min(1, { message: 'La calificación mínima es 1' }) 15 | @Max(5, { message: 'La calificación máxima es 5' }) 16 | rating: number; 17 | 18 | @IsNotEmpty({ message: 'El comentario es requerido' }) 19 | @IsString({ message: 'El comentario debe ser una cadena de texto' }) 20 | comment: string; 21 | } 22 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import eslint from '@eslint/js'; 3 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; 4 | import globals from 'globals'; 5 | import tseslint from 'typescript-eslint'; 6 | 7 | export default tseslint.config( 8 | { 9 | ignores: ['eslint.config.mjs'], 10 | }, 11 | eslint.configs.recommended, 12 | ...tseslint.configs.recommendedTypeChecked, 13 | eslintPluginPrettierRecommended, 14 | { 15 | languageOptions: { 16 | globals: { 17 | ...globals.node, 18 | ...globals.jest, 19 | }, 20 | sourceType: 'commonjs', 21 | parserOptions: { 22 | projectService: true, 23 | tsconfigRootDir: import.meta.dirname, 24 | }, 25 | }, 26 | }, 27 | { 28 | rules: { 29 | '@typescript-eslint/no-explicit-any': 'off', 30 | '@typescript-eslint/no-floating-promises': 'warn', 31 | '@typescript-eslint/no-unsafe-argument': 'warn' 32 | }, 33 | }, 34 | ); -------------------------------------------------------------------------------- /microservices/api-gateway/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { ValidationPipe } from '@nestjs/common'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule); 7 | 8 | // Habilitar CORS para permitir conexiones desde diferentes orígenes 9 | app.enableCors({ 10 | origin: true, 11 | credentials: true, 12 | }); 13 | 14 | // Configurar validación global 15 | app.useGlobalPipes(new ValidationPipe({ 16 | transform: true, 17 | whitelist: true, 18 | forbidNonWhitelisted: true, 19 | })); 20 | 21 | // Prefijo global para todas las rutas 22 | app.setGlobalPrefix('api/v1'); 23 | 24 | const port = process.env.PORT || 3000; 25 | await app.listen(port); 26 | 27 | console.log(`🚀 API Gateway ejecutándose en puerto ${port}`); 28 | console.log(`📊 Documentación disponible en http://localhost:${port}/api/v1`); 29 | } 30 | 31 | bootstrap(); 32 | -------------------------------------------------------------------------------- /microservices/api-gateway/src/modules/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, Body, UseGuards, Get, Request } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import { JwtAuthGuard } from './jwt-auth.guard'; 4 | 5 | @Controller('auth') 6 | export class AuthController { 7 | constructor(private readonly authService: AuthService) {} 8 | 9 | @Post('login') 10 | async login(@Body() loginDto: { email: string; password: string }) { 11 | return this.authService.login(loginDto); 12 | } 13 | 14 | @Post('register') 15 | async register(@Body() registerDto: any) { 16 | return this.authService.register(registerDto); 17 | } 18 | 19 | @Get('profile') 20 | @UseGuards(JwtAuthGuard) 21 | async getProfile(@Request() req: any) { 22 | return this.authService.getProfile(req.user.id); 23 | } 24 | 25 | @Post('refresh') 26 | @UseGuards(JwtAuthGuard) 27 | async refresh(@Request() req: any) { 28 | return this.authService.refreshToken(req.user); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /microservices/product-service/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | import { JwtModule } from '@nestjs/jwt'; 4 | import { PassportModule } from '@nestjs/passport'; 5 | 6 | import { AppController } from './app.controller'; 7 | import { AppService } from './app.service'; 8 | import { ProductModule } from './modules/product/product.module'; 9 | import { CategoryModule } from './modules/category/category.module'; 10 | 11 | @Module({ 12 | imports: [ 13 | MongooseModule.forRoot( 14 | process.env.MONGODB_URI || 'mongodb://mongo:mongo123@localhost:27017/productdb?authSource=admin' 15 | ), 16 | PassportModule, 17 | JwtModule.register({ 18 | secret: process.env.JWT_SECRET || 'your-super-secret-jwt-key', 19 | signOptions: { expiresIn: '24h' }, 20 | }), 21 | ProductModule, 22 | CategoryModule, 23 | ], 24 | controllers: [AppController], 25 | providers: [AppService], 26 | }) 27 | export class AppModule {} 28 | -------------------------------------------------------------------------------- /microservices/product-service/src/modules/category/dto/create-category.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString, IsOptional, IsBoolean, IsObject } from 'class-validator'; 2 | 3 | export class CreateCategoryDto { 4 | @IsNotEmpty({ message: 'El nombre es requerido' }) 5 | @IsString({ message: 'El nombre debe ser una cadena de texto' }) 6 | name: string; 7 | 8 | @IsNotEmpty({ message: 'La descripción es requerida' }) 9 | @IsString({ message: 'La descripción debe ser una cadena de texto' }) 10 | description: string; 11 | 12 | @IsNotEmpty({ message: 'El slug es requerido' }) 13 | @IsString({ message: 'El slug debe ser una cadena de texto' }) 14 | slug: string; 15 | 16 | @IsOptional() 17 | @IsBoolean({ message: 'isActive debe ser un valor booleano' }) 18 | isActive?: boolean; 19 | 20 | @IsOptional() 21 | @IsString({ message: 'La imagen debe ser una cadena de texto' }) 22 | image?: string; 23 | 24 | @IsOptional() 25 | @IsObject({ message: 'Los metadatos deben ser un objeto' }) 26 | metadata?: Record; 27 | } 28 | -------------------------------------------------------------------------------- /microservices/api-gateway/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { HttpModule } from '@nestjs/axios'; 3 | import { JwtModule } from '@nestjs/jwt'; 4 | import { PassportModule } from '@nestjs/passport'; 5 | 6 | import { AppController } from './app.controller'; 7 | import { AppService } from './app.service'; 8 | import { UserModule } from './modules/user/user.module'; 9 | import { ProductModule } from './modules/product/product.module'; 10 | import { NotificationModule } from './modules/notification/notification.module'; 11 | import { AuthModule } from './modules/auth/auth.module'; 12 | 13 | @Module({ 14 | imports: [ 15 | HttpModule, 16 | PassportModule, 17 | JwtModule.register({ 18 | secret: process.env.JWT_SECRET || 'your-super-secret-jwt-key', 19 | signOptions: { expiresIn: '24h' }, 20 | }), 21 | UserModule, 22 | ProductModule, 23 | NotificationModule, 24 | AuthModule, 25 | ], 26 | controllers: [AppController], 27 | providers: [AppService], 28 | }) 29 | export class AppModule {} 30 | -------------------------------------------------------------------------------- /shared/config/database.config.ts: -------------------------------------------------------------------------------- 1 | export interface DatabaseConfig { 2 | type: 'postgres' | 'mongodb' | 'redis'; 3 | host: string; 4 | port: number; 5 | username?: string; 6 | password?: string; 7 | database: string; 8 | uri?: string; 9 | } 10 | 11 | export const databaseConfigs = { 12 | postgres: { 13 | type: 'postgres' as const, 14 | host: process.env.DATABASE_HOST || 'localhost', 15 | port: parseInt(process.env.DATABASE_PORT) || 5432, 16 | username: process.env.DATABASE_USER || 'postgres', 17 | password: process.env.DATABASE_PASSWORD || 'postgres123', 18 | database: process.env.DATABASE_NAME || 'userdb', 19 | }, 20 | 21 | mongodb: { 22 | type: 'mongodb' as const, 23 | uri: process.env.MONGODB_URI || 'mongodb://mongo:mongo123@localhost:27017/productdb?authSource=admin', 24 | }, 25 | 26 | redis: { 27 | type: 'redis' as const, 28 | host: process.env.REDIS_HOST || 'localhost', 29 | port: parseInt(process.env.REDIS_PORT) || 6379, 30 | password: process.env.REDIS_PASSWORD || 'redis123', 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | # Configuración general 2 | NODE_ENV=development 3 | JWT_SECRET=your-super-secret-jwt-key 4 | 5 | # API Gateway 6 | API_GATEWAY_PORT=3000 7 | USER_SERVICE_URL=http://user-service:3001 8 | PRODUCT_SERVICE_URL=http://product-service:3002 9 | NOTIFICATION_SERVICE_URL=http://notification-service:3003 10 | 11 | # User Service 12 | USER_SERVICE_PORT=3001 13 | DATABASE_HOST=postgres 14 | DATABASE_PORT=5432 15 | DATABASE_NAME=userdb 16 | DATABASE_USER=postgres 17 | DATABASE_PASSWORD=postgres123 18 | 19 | # Product Service 20 | PRODUCT_SERVICE_PORT=3002 21 | MONGODB_URI=mongodb://mongo:mongo123@mongodb:27017/productdb?authSource=admin 22 | 23 | # Notification Service 24 | NOTIFICATION_SERVICE_PORT=3003 25 | REDIS_HOST=redis 26 | REDIS_PORT=6379 27 | REDIS_PASSWORD=redis123 28 | 29 | # Base de datos PostgreSQL 30 | POSTGRES_DB=userdb 31 | POSTGRES_USER=postgres 32 | POSTGRES_PASSWORD=postgres123 33 | 34 | # Base de datos MongoDB 35 | MONGO_INITDB_ROOT_USERNAME=mongo 36 | MONGO_INITDB_ROOT_PASSWORD=mongo123 37 | MONGO_INITDB_DATABASE=productdb 38 | 39 | # Redis 40 | REDIS_REQUIREPASS=redis123 41 | -------------------------------------------------------------------------------- /microservices/api-gateway/src/modules/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards } from '@nestjs/common'; 2 | import { UserService } from './user.service'; 3 | import { JwtAuthGuard } from '../auth/jwt-auth.guard'; 4 | 5 | @Controller('users') 6 | export class UserController { 7 | constructor(private readonly userService: UserService) {} 8 | 9 | @Get() 10 | async findAll(@Query() query: any) { 11 | return this.userService.findAll(query); 12 | } 13 | 14 | @Get(':id') 15 | async findOne(@Param('id') id: string) { 16 | return this.userService.findOne(id); 17 | } 18 | 19 | @Post() 20 | async create(@Body() createUserDto: any) { 21 | return this.userService.create(createUserDto); 22 | } 23 | 24 | @Put(':id') 25 | @UseGuards(JwtAuthGuard) 26 | async update(@Param('id') id: string, @Body() updateUserDto: any) { 27 | return this.userService.update(id, updateUserDto); 28 | } 29 | 30 | @Delete(':id') 31 | @UseGuards(JwtAuthGuard) 32 | async remove(@Param('id') id: string) { 33 | return this.userService.remove(id); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /microservices/api-gateway/src/modules/notification/notification.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Post, Body, Param, UseGuards, Request } from '@nestjs/common'; 2 | import { NotificationService } from './notification.service'; 3 | import { JwtAuthGuard } from '../auth/jwt-auth.guard'; 4 | 5 | @Controller('notifications') 6 | export class NotificationController { 7 | constructor(private readonly notificationService: NotificationService) {} 8 | 9 | @Get() 10 | @UseGuards(JwtAuthGuard) 11 | async findAll(@Request() req: any) { 12 | return this.notificationService.findAll(req.user.id); 13 | } 14 | 15 | @Post('send') 16 | @UseGuards(JwtAuthGuard) 17 | async send(@Body() sendNotificationDto: any) { 18 | return this.notificationService.send(sendNotificationDto); 19 | } 20 | 21 | @Post(':id/read') 22 | @UseGuards(JwtAuthGuard) 23 | async markAsRead(@Param('id') id: string, @Request() req: any) { 24 | return this.notificationService.markAsRead(id, req.user.id); 25 | } 26 | 27 | @Get('unread-count') 28 | @UseGuards(JwtAuthGuard) 29 | async getUnreadCount(@Request() req: any) { 30 | return this.notificationService.getUnreadCount(req.user.id); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /microservices/user-service/src/modules/user/dto/create-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsNotEmpty, IsOptional, IsString, MinLength, IsArray } from 'class-validator'; 2 | 3 | export class CreateUserDto { 4 | @IsEmail({}, { message: 'El email debe tener un formato válido' }) 5 | @IsNotEmpty({ message: 'El email es requerido' }) 6 | email: string; 7 | 8 | @IsString({ message: 'La contraseña debe ser una cadena de texto' }) 9 | @MinLength(6, { message: 'La contraseña debe tener al menos 6 caracteres' }) 10 | @IsNotEmpty({ message: 'La contraseña es requerida' }) 11 | password: string; 12 | 13 | @IsString({ message: 'El nombre debe ser una cadena de texto' }) 14 | @IsNotEmpty({ message: 'El nombre es requerido' }) 15 | name: string; 16 | 17 | @IsOptional() 18 | @IsString({ message: 'El apellido debe ser una cadena de texto' }) 19 | lastName?: string; 20 | 21 | @IsOptional() 22 | @IsString({ message: 'El teléfono debe ser una cadena de texto' }) 23 | phone?: string; 24 | 25 | @IsOptional() 26 | @IsString({ message: 'El avatar debe ser una cadena de texto' }) 27 | avatar?: string; 28 | 29 | @IsOptional() 30 | @IsArray({ message: 'Los roles deben ser un array' }) 31 | roles?: string[]; 32 | } 33 | -------------------------------------------------------------------------------- /microservices/notification-service/src/modules/notification/dto/send-notification.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsNotEmpty, 3 | IsString, 4 | IsOptional, 5 | IsArray, 6 | IsBoolean, 7 | IsEnum, 8 | IsObject, 9 | } from 'class-validator'; 10 | import { NotificationType } from '../interfaces/notification.interface'; 11 | 12 | export class SendNotificationDto { 13 | @IsOptional() 14 | @IsString({ message: 'El ID de usuario debe ser una cadena de texto' }) 15 | userId?: string; 16 | 17 | @IsOptional() 18 | @IsArray({ message: 'Los IDs de usuarios deben ser un array' }) 19 | userIds?: string[]; 20 | 21 | @IsNotEmpty({ message: 'El título es requerido' }) 22 | @IsString({ message: 'El título debe ser una cadena de texto' }) 23 | title: string; 24 | 25 | @IsNotEmpty({ message: 'El mensaje es requerido' }) 26 | @IsString({ message: 'El mensaje debe ser una cadena de texto' }) 27 | message: string; 28 | 29 | @IsNotEmpty({ message: 'El tipo es requerido' }) 30 | @IsEnum(NotificationType, { message: 'El tipo debe ser un valor válido' }) 31 | type: NotificationType; 32 | 33 | @IsOptional() 34 | @IsObject({ message: 'Los datos adicionales deben ser un objeto' }) 35 | data?: Record; 36 | 37 | @IsOptional() 38 | @IsBoolean({ message: 'broadcast debe ser un valor booleano' }) 39 | broadcast?: boolean; 40 | } 41 | -------------------------------------------------------------------------------- /microservices/user-service/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { JwtModule } from '@nestjs/jwt'; 4 | import { PassportModule } from '@nestjs/passport'; 5 | 6 | import { AppController } from './app.controller'; 7 | import { AppService } from './app.service'; 8 | import { UserModule } from './modules/user/user.module'; 9 | import { AuthModule } from './modules/auth/auth.module'; 10 | import { User } from './modules/user/entities/user.entity'; 11 | 12 | @Module({ 13 | imports: [ 14 | TypeOrmModule.forRoot({ 15 | type: 'postgres', 16 | host: process.env.DATABASE_HOST || 'localhost', 17 | port: parseInt(process.env.DATABASE_PORT || '5432'), 18 | username: process.env.DATABASE_USER || 'postgres', 19 | password: process.env.DATABASE_PASSWORD || 'postgres123', 20 | database: process.env.DATABASE_NAME || 'userdb', 21 | entities: [User], 22 | synchronize: process.env.NODE_ENV === 'development', // Solo en desarrollo 23 | logging: process.env.NODE_ENV === 'development', 24 | }), 25 | PassportModule, 26 | JwtModule.register({ 27 | secret: process.env.JWT_SECRET || 'your-super-secret-jwt-key', 28 | signOptions: { expiresIn: '24h' }, 29 | }), 30 | UserModule, 31 | AuthModule, 32 | ], 33 | controllers: [AppController], 34 | providers: [AppService], 35 | }) 36 | export class AppModule {} 37 | -------------------------------------------------------------------------------- /scripts/start-dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script para iniciar todos los servicios en modo desarrollo 4 | 5 | echo "🚀 Iniciando microservicios en modo desarrollo..." 6 | 7 | # Verificar si Docker está corriendo 8 | if ! docker info > /dev/null 2>&1; then 9 | echo "❌ Docker no está corriendo. Por favor, inicia Docker primero." 10 | exit 1 11 | fi 12 | 13 | # Crear red si no existe 14 | docker network create microservices-network 2>/dev/null || true 15 | 16 | echo "📦 Iniciando bases de datos..." 17 | docker-compose up -d postgres mongodb redis 18 | 19 | echo "⏳ Esperando que las bases de datos estén listas..." 20 | sleep 10 21 | 22 | echo "🔧 Iniciando microservicios..." 23 | docker-compose up -d api-gateway user-service product-service notification-service 24 | 25 | echo "✅ Todos los servicios están iniciados!" 26 | echo "" 27 | echo "📋 Servicios disponibles:" 28 | echo " 🌐 API Gateway: http://localhost:3000" 29 | echo " 👤 User Service: http://localhost:3001" 30 | echo " 📦 Product Service: http://localhost:3002" 31 | echo " 🔔 Notification Service: http://localhost:3003" 32 | echo "" 33 | echo "📊 Bases de datos:" 34 | echo " 🐘 PostgreSQL: localhost:5432 (user: postgres, pass: postgres123)" 35 | echo " 🍃 MongoDB: localhost:27017 (user: mongo, pass: mongo123)" 36 | echo " 🔴 Redis: localhost:6379 (pass: redis123)" 37 | echo "" 38 | echo "📝 Para ver logs: docker-compose logs -f [servicio]" 39 | echo "🛑 Para detener: ./scripts/stop-dev.sh" 40 | -------------------------------------------------------------------------------- /microservices/user-service/src/modules/user/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | Column, 4 | PrimaryGeneratedColumn, 5 | CreateDateColumn, 6 | UpdateDateColumn, 7 | BeforeInsert, 8 | BeforeUpdate, 9 | } from 'typeorm'; 10 | import * as bcrypt from 'bcryptjs'; 11 | 12 | @Entity('users') 13 | export class User { 14 | @PrimaryGeneratedColumn('uuid') 15 | id: string; 16 | 17 | @Column({ unique: true }) 18 | email: string; 19 | 20 | @Column() 21 | password: string; 22 | 23 | @Column() 24 | name: string; 25 | 26 | @Column({ nullable: true }) 27 | lastName: string; 28 | 29 | @Column({ nullable: true }) 30 | phone: string; 31 | 32 | @Column({ nullable: true }) 33 | avatar: string; 34 | 35 | @Column({ type: 'simple-array', default: 'user' }) 36 | roles: string[]; 37 | 38 | @Column({ default: true }) 39 | isActive: boolean; 40 | 41 | @Column({ nullable: true }) 42 | lastLogin: Date; 43 | 44 | @CreateDateColumn() 45 | createdAt: Date; 46 | 47 | @UpdateDateColumn() 48 | updatedAt: Date; 49 | 50 | @BeforeInsert() 51 | @BeforeUpdate() 52 | async hashPassword() { 53 | if (this.password) { 54 | const salt = await bcrypt.genSalt(10); 55 | this.password = await bcrypt.hash(this.password, salt); 56 | } 57 | } 58 | 59 | async validatePassword(password: string): Promise { 60 | return bcrypt.compare(password, this.password); 61 | } 62 | 63 | toJSON() { 64 | const { password, ...result } = this; 65 | return result; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /microservices/api-gateway/src/modules/product/product.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards } from '@nestjs/common'; 2 | import { ProductService } from './product.service'; 3 | import { JwtAuthGuard } from '../auth/jwt-auth.guard'; 4 | 5 | @Controller('products') 6 | export class ProductController { 7 | constructor(private readonly productService: ProductService) {} 8 | 9 | @Get() 10 | async findAll(@Query() query: any) { 11 | return this.productService.findAll(query); 12 | } 13 | 14 | @Get(':id') 15 | async findOne(@Param('id') id: string) { 16 | return this.productService.findOne(id); 17 | } 18 | 19 | @Post() 20 | @UseGuards(JwtAuthGuard) 21 | async create(@Body() createProductDto: any) { 22 | return this.productService.create(createProductDto); 23 | } 24 | 25 | @Put(':id') 26 | @UseGuards(JwtAuthGuard) 27 | async update(@Param('id') id: string, @Body() updateProductDto: any) { 28 | return this.productService.update(id, updateProductDto); 29 | } 30 | 31 | @Delete(':id') 32 | @UseGuards(JwtAuthGuard) 33 | async remove(@Param('id') id: string) { 34 | return this.productService.remove(id); 35 | } 36 | 37 | @Get('category/:category') 38 | async findByCategory(@Param('category') category: string, @Query() query: any) { 39 | return this.productService.findByCategory(category, query); 40 | } 41 | 42 | @Post(':id/reviews') 43 | @UseGuards(JwtAuthGuard) 44 | async addReview(@Param('id') id: string, @Body() reviewDto: any) { 45 | return this.productService.addReview(id, reviewDto); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /microservices/product-service/src/modules/category/category.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Patch, 7 | Param, 8 | Delete, 9 | Query, 10 | } from '@nestjs/common'; 11 | import { CategoryService } from './category.service'; 12 | import { CreateCategoryDto } from './dto/create-category.dto'; 13 | import { UpdateCategoryDto } from './dto/update-category.dto'; 14 | 15 | @Controller('categories') 16 | export class CategoryController { 17 | constructor(private readonly categoryService: CategoryService) {} 18 | 19 | @Post() 20 | create(@Body() createCategoryDto: CreateCategoryDto) { 21 | return this.categoryService.create(createCategoryDto); 22 | } 23 | 24 | @Get() 25 | findAll( 26 | @Query('page') page: number = 1, 27 | @Query('limit') limit: number = 10, 28 | @Query('search') search?: string, 29 | @Query('active') active?: boolean, 30 | ) { 31 | return this.categoryService.findAll(page, limit, search, active); 32 | } 33 | 34 | @Get(':id') 35 | findOne(@Param('id') id: string) { 36 | return this.categoryService.findOne(id); 37 | } 38 | 39 | @Get('slug/:slug') 40 | findBySlug(@Param('slug') slug: string) { 41 | return this.categoryService.findBySlug(slug); 42 | } 43 | 44 | @Patch(':id') 45 | update(@Param('id') id: string, @Body() updateCategoryDto: UpdateCategoryDto) { 46 | return this.categoryService.update(id, updateCategoryDto); 47 | } 48 | 49 | @Delete(':id') 50 | remove(@Param('id') id: string) { 51 | return this.categoryService.remove(id); 52 | } 53 | 54 | @Patch(':id/activate') 55 | activate(@Param('id') id: string) { 56 | return this.categoryService.activate(id); 57 | } 58 | 59 | @Patch(':id/deactivate') 60 | deactivate(@Param('id') id: string) { 61 | return this.categoryService.deactivate(id); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /microservices/user-service/src/modules/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Patch, 7 | Param, 8 | Delete, 9 | Query, 10 | UseGuards, 11 | ParseUUIDPipe, 12 | } from '@nestjs/common'; 13 | import { UserService } from './user.service'; 14 | import { CreateUserDto } from './dto/create-user.dto'; 15 | import { UpdateUserDto } from './dto/update-user.dto'; 16 | import { JwtAuthGuard } from '../auth/jwt-auth.guard'; 17 | 18 | @Controller('users') 19 | export class UserController { 20 | constructor(private readonly userService: UserService) {} 21 | 22 | @Post() 23 | create(@Body() createUserDto: CreateUserDto) { 24 | return this.userService.create(createUserDto); 25 | } 26 | 27 | @Get() 28 | findAll( 29 | @Query('page') page: number = 1, 30 | @Query('limit') limit: number = 10, 31 | @Query('search') search?: string, 32 | ) { 33 | return this.userService.findAll(page, limit, search); 34 | } 35 | 36 | @Get(':id') 37 | findOne(@Param('id', ParseUUIDPipe) id: string) { 38 | return this.userService.findOne(id); 39 | } 40 | 41 | @Patch(':id') 42 | @UseGuards(JwtAuthGuard) 43 | update( 44 | @Param('id', ParseUUIDPipe) id: string, 45 | @Body() updateUserDto: UpdateUserDto, 46 | ) { 47 | return this.userService.update(id, updateUserDto); 48 | } 49 | 50 | @Delete(':id') 51 | @UseGuards(JwtAuthGuard) 52 | remove(@Param('id', ParseUUIDPipe) id: string) { 53 | return this.userService.remove(id); 54 | } 55 | 56 | @Get('email/:email') 57 | findByEmail(@Param('email') email: string) { 58 | return this.userService.findByEmail(email); 59 | } 60 | 61 | @Patch(':id/activate') 62 | @UseGuards(JwtAuthGuard) 63 | activate(@Param('id', ParseUUIDPipe) id: string) { 64 | return this.userService.activate(id); 65 | } 66 | 67 | @Patch(':id/deactivate') 68 | @UseGuards(JwtAuthGuard) 69 | deactivate(@Param('id', ParseUUIDPipe) id: string) { 70 | return this.userService.deactivate(id); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /scripts/quick-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 🚀 Pruebas rápidas del sistema de microservicios 4 | # Uso: ./scripts/quick-test.sh 5 | 6 | echo "🚀 Pruebas rápidas del sistema..." 7 | echo "================================" 8 | 9 | # Verificar servicios 10 | echo "📊 Verificando servicios..." 11 | curl -s http://localhost:3000/api/v1/health > /dev/null && echo "✅ API Gateway" || echo "❌ API Gateway" 12 | curl -s http://localhost:3001/health > /dev/null && echo "✅ User Service" || echo "❌ User Service" 13 | curl -s http://localhost:3002/health > /dev/null && echo "✅ Product Service" || echo "❌ Product Service" 14 | curl -s http://localhost:3003/health > /dev/null && echo "✅ Notification Service" || echo "❌ Notification Service" 15 | 16 | echo "" 17 | 18 | # Información de servicios 19 | echo "📋 Información de servicios:" 20 | echo "API Gateway:" 21 | curl -s http://localhost:3000/api/v1 | jq -r '.name + " - " + .status' 22 | 23 | echo "User Service:" 24 | curl -s http://localhost:3001 | jq -r '.name + " - " + .status' 25 | 26 | echo "Product Service:" 27 | curl -s http://localhost:3002 | jq -r '.name + " - " + .status' 28 | 29 | echo "Notification Service:" 30 | curl -s http://localhost:3003 | jq -r '.name + " - " + .status' 31 | 32 | echo "" 33 | 34 | # Estado de contenedores 35 | echo "🐳 Estado de contenedores:" 36 | docker-compose ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}" 37 | 38 | echo "" 39 | 40 | # Verificar bases de datos 41 | echo "🗄️ Verificando bases de datos..." 42 | 43 | # PostgreSQL 44 | if docker exec microservices-postgres pg_isready -U postgres > /dev/null 2>&1; then 45 | echo "✅ PostgreSQL - Conectado" 46 | else 47 | echo "❌ PostgreSQL - Desconectado" 48 | fi 49 | 50 | # MongoDB 51 | if docker exec microservices-mongo mongosh -u mongo -p mongo123 --quiet --eval "db.runCommand({ping: 1})" > /dev/null 2>&1; then 52 | echo "✅ MongoDB - Conectado" 53 | else 54 | echo "❌ MongoDB - Desconectado" 55 | fi 56 | 57 | # Redis 58 | if docker exec microservices-redis redis-cli -a redis123 ping > /dev/null 2>&1; then 59 | echo "✅ Redis - Conectado" 60 | else 61 | echo "❌ Redis - Desconectado" 62 | fi 63 | 64 | echo "" 65 | echo "🎉 Pruebas rápidas completadas!" 66 | echo "" 67 | echo "💡 Para pruebas completas ejecuta: ./scripts/test-all.sh" 68 | echo "📖 Para ver todos los comandos: cat test-endpoints.md" 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "base_microservicios", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json" 21 | }, 22 | "dependencies": { 23 | "@nestjs/common": "^11.0.1", 24 | "@nestjs/core": "^11.0.1", 25 | "@nestjs/platform-express": "^11.0.1", 26 | "reflect-metadata": "^0.2.2", 27 | "rxjs": "^7.8.1" 28 | }, 29 | "devDependencies": { 30 | "@eslint/eslintrc": "^3.2.0", 31 | "@eslint/js": "^9.18.0", 32 | "@nestjs/cli": "^11.0.0", 33 | "@nestjs/schematics": "^11.0.0", 34 | "@nestjs/testing": "^11.0.1", 35 | "@swc/cli": "^0.6.0", 36 | "@swc/core": "^1.10.7", 37 | "@types/express": "^5.0.0", 38 | "@types/jest": "^29.5.14", 39 | "@types/node": "^22.10.7", 40 | "@types/supertest": "^6.0.2", 41 | "eslint": "^9.18.0", 42 | "eslint-config-prettier": "^10.0.1", 43 | "eslint-plugin-prettier": "^5.2.2", 44 | "globals": "^16.0.0", 45 | "jest": "^29.7.0", 46 | "prettier": "^3.4.2", 47 | "source-map-support": "^0.5.21", 48 | "supertest": "^7.0.0", 49 | "ts-jest": "^29.2.5", 50 | "ts-loader": "^9.5.2", 51 | "ts-node": "^10.9.2", 52 | "tsconfig-paths": "^4.2.0", 53 | "typescript": "^5.7.3", 54 | "typescript-eslint": "^8.20.0" 55 | }, 56 | "jest": { 57 | "moduleFileExtensions": [ 58 | "js", 59 | "json", 60 | "ts" 61 | ], 62 | "rootDir": "src", 63 | "testRegex": ".*\\.spec\\.ts$", 64 | "transform": { 65 | "^.+\\.(t|j)s$": "ts-jest" 66 | }, 67 | "collectCoverageFrom": [ 68 | "**/*.(t|j)s" 69 | ], 70 | "coverageDirectory": "../coverage", 71 | "testEnvironment": "node" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /microservices/notification-service/src/modules/notification/notification.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Param, 7 | Query, 8 | HttpCode, 9 | HttpStatus, 10 | } from '@nestjs/common'; 11 | import { NotificationService } from './notification.service'; 12 | import { SendNotificationDto } from './dto/send-notification.dto'; 13 | 14 | @Controller('notifications') 15 | export class NotificationController { 16 | constructor(private readonly notificationService: NotificationService) {} 17 | 18 | @Post('send') 19 | @HttpCode(HttpStatus.OK) 20 | async sendNotification(@Body() sendNotificationDto: SendNotificationDto) { 21 | return this.notificationService.sendNotification(sendNotificationDto); 22 | } 23 | 24 | @Get() 25 | async getNotifications( 26 | @Query('userId') userId: string, 27 | @Query('offset') offset: number = 0, 28 | @Query('limit') limit: number = 20, 29 | ) { 30 | if (!userId) { 31 | return { 32 | error: 'El parámetro userId es requerido', 33 | statusCode: 400, 34 | }; 35 | } 36 | 37 | return this.notificationService.getNotifications(userId, offset, limit); 38 | } 39 | 40 | @Post(':id/read') 41 | @HttpCode(HttpStatus.OK) 42 | async markAsRead(@Param('id') notificationId: string, @Body('userId') userId: string) { 43 | if (!userId) { 44 | return { 45 | error: 'El userId es requerido', 46 | statusCode: 400, 47 | }; 48 | } 49 | 50 | return this.notificationService.markAsRead(notificationId, userId); 51 | } 52 | 53 | @Get('unread-count') 54 | async getUnreadCount(@Query('userId') userId: string) { 55 | if (!userId) { 56 | return { 57 | error: 'El parámetro userId es requerido', 58 | statusCode: 400, 59 | }; 60 | } 61 | 62 | return this.notificationService.getUnreadCount(userId); 63 | } 64 | 65 | @Get('stats') 66 | async getStats() { 67 | return this.notificationService.getStats(); 68 | } 69 | 70 | @Post('broadcast') 71 | @HttpCode(HttpStatus.OK) 72 | async broadcast(@Body() broadcastDto: Omit) { 73 | return this.notificationService.broadcast(broadcastDto); 74 | } 75 | 76 | @Get('connected-users') 77 | async getConnectedUsers() { 78 | return this.notificationService.getConnectedUsers(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /microservices/product-service/src/modules/product/dto/create-product.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsNotEmpty, 3 | IsString, 4 | IsNumber, 5 | IsOptional, 6 | IsArray, 7 | IsBoolean, 8 | IsObject, 9 | Min, 10 | IsMongoId, 11 | } from 'class-validator'; 12 | 13 | export class CreateProductDto { 14 | @IsNotEmpty({ message: 'El nombre es requerido' }) 15 | @IsString({ message: 'El nombre debe ser una cadena de texto' }) 16 | name: string; 17 | 18 | @IsNotEmpty({ message: 'La descripción es requerida' }) 19 | @IsString({ message: 'La descripción debe ser una cadena de texto' }) 20 | description: string; 21 | 22 | @IsNotEmpty({ message: 'El SKU es requerido' }) 23 | @IsString({ message: 'El SKU debe ser una cadena de texto' }) 24 | sku: string; 25 | 26 | @IsNotEmpty({ message: 'El precio es requerido' }) 27 | @IsNumber({}, { message: 'El precio debe ser un número' }) 28 | @Min(0, { message: 'El precio debe ser mayor o igual a 0' }) 29 | price: number; 30 | 31 | @IsOptional() 32 | @IsNumber({}, { message: 'El precio de comparación debe ser un número' }) 33 | @Min(0, { message: 'El precio de comparación debe ser mayor o igual a 0' }) 34 | comparePrice?: number; 35 | 36 | @IsNotEmpty({ message: 'El stock es requerido' }) 37 | @IsNumber({}, { message: 'El stock debe ser un número' }) 38 | @Min(0, { message: 'El stock debe ser mayor o igual a 0' }) 39 | stock: number; 40 | 41 | @IsNotEmpty({ message: 'El ID de categoría es requerido' }) 42 | @IsMongoId({ message: 'El ID de categoría debe ser un ObjectId válido' }) 43 | categoryId: string; 44 | 45 | @IsOptional() 46 | @IsArray({ message: 'Las imágenes deben ser un array' }) 47 | images?: string[]; 48 | 49 | @IsOptional() 50 | @IsArray({ message: 'Las etiquetas deben ser un array' }) 51 | tags?: string[]; 52 | 53 | @IsOptional() 54 | @IsBoolean({ message: 'isActive debe ser un valor booleano' }) 55 | isActive?: boolean; 56 | 57 | @IsOptional() 58 | @IsBoolean({ message: 'isFeatured debe ser un valor booleano' }) 59 | isFeatured?: boolean; 60 | 61 | @IsOptional() 62 | @IsObject({ message: 'Las especificaciones deben ser un objeto' }) 63 | specifications?: Record; 64 | 65 | @IsOptional() 66 | @IsObject({ message: 'Los datos SEO deben ser un objeto' }) 67 | seoData?: { 68 | title?: string; 69 | description?: string; 70 | keywords?: string[]; 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /microservices/api-gateway/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-gateway", 3 | "version": "1.0.0", 4 | "description": "API Gateway para microservicios", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json" 21 | }, 22 | "dependencies": { 23 | "@nestjs/common": "^11.0.1", 24 | "@nestjs/core": "^11.0.1", 25 | "@nestjs/platform-express": "^11.0.1", 26 | "@nestjs/axios": "^3.1.3", 27 | "@nestjs/jwt": "^10.2.0", 28 | "@nestjs/passport": "^10.0.3", 29 | "passport": "^0.6.0", 30 | "passport-jwt": "^4.0.1", 31 | "axios": "^1.6.0", 32 | "class-validator": "^0.14.0", 33 | "class-transformer": "^0.5.1", 34 | "reflect-metadata": "^0.2.2", 35 | "rxjs": "^7.8.1" 36 | }, 37 | "devDependencies": { 38 | "@nestjs/cli": "^11.0.0", 39 | "@nestjs/schematics": "^11.0.0", 40 | "@nestjs/testing": "^11.0.1", 41 | "@types/express": "^5.0.0", 42 | "@types/jest": "^29.5.14", 43 | "@types/node": "^22.10.7", 44 | "@types/passport-jwt": "^3.0.13", 45 | "@types/supertest": "^6.0.2", 46 | "eslint": "^9.18.0", 47 | "jest": "^29.7.0", 48 | "prettier": "^3.4.2", 49 | "source-map-support": "^0.5.21", 50 | "supertest": "^7.0.0", 51 | "ts-jest": "^29.2.5", 52 | "ts-loader": "^9.5.2", 53 | "ts-node": "^10.9.2", 54 | "tsconfig-paths": "^4.2.0", 55 | "typescript": "^5.7.3" 56 | }, 57 | "jest": { 58 | "moduleFileExtensions": [ 59 | "js", 60 | "json", 61 | "ts" 62 | ], 63 | "rootDir": "src", 64 | "testRegex": ".*\\.spec\\.ts$", 65 | "transform": { 66 | "^.+\\.(t|j)s$": "ts-jest" 67 | }, 68 | "collectCoverageFrom": [ 69 | "**/*.(t|j)s" 70 | ], 71 | "coverageDirectory": "../coverage", 72 | "testEnvironment": "node" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /microservices/product-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "product-service", 3 | "version": "1.0.0", 4 | "description": "Microservicio de gestión de productos", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json" 21 | }, 22 | "dependencies": { 23 | "@nestjs/common": "^11.0.1", 24 | "@nestjs/core": "^11.0.1", 25 | "@nestjs/platform-express": "^11.0.1", 26 | "@nestjs/mapped-types": "^2.0.5", 27 | "@nestjs/mongoose": "^10.1.0", 28 | "@nestjs/jwt": "^10.2.0", 29 | "@nestjs/passport": "^10.0.3", 30 | "mongoose": "^8.0.3", 31 | "passport": "^0.6.0", 32 | "passport-jwt": "^4.0.1", 33 | "class-validator": "^0.14.0", 34 | "class-transformer": "^0.5.1", 35 | "reflect-metadata": "^0.2.2", 36 | "rxjs": "^7.8.1" 37 | }, 38 | "devDependencies": { 39 | "@nestjs/cli": "^11.0.0", 40 | "@nestjs/schematics": "^11.0.0", 41 | "@nestjs/testing": "^11.0.1", 42 | "@types/express": "^5.0.0", 43 | "@types/jest": "^29.5.14", 44 | "@types/node": "^22.10.7", 45 | "@types/passport-jwt": "^3.0.13", 46 | "@types/supertest": "^6.0.2", 47 | "eslint": "^9.18.0", 48 | "jest": "^29.7.0", 49 | "prettier": "^3.4.2", 50 | "source-map-support": "^0.5.21", 51 | "supertest": "^7.0.0", 52 | "ts-jest": "^29.2.5", 53 | "ts-loader": "^9.5.2", 54 | "ts-node": "^10.9.2", 55 | "tsconfig-paths": "^4.2.0", 56 | "typescript": "^5.7.3" 57 | }, 58 | "jest": { 59 | "moduleFileExtensions": [ 60 | "js", 61 | "json", 62 | "ts" 63 | ], 64 | "rootDir": "src", 65 | "testRegex": ".*\\.spec\\.ts$", 66 | "transform": { 67 | "^.+\\.(t|j)s$": "ts-jest" 68 | }, 69 | "collectCoverageFrom": [ 70 | "**/*.(t|j)s" 71 | ], 72 | "coverageDirectory": "../coverage", 73 | "testEnvironment": "node" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /microservices/product-service/src/modules/product/schemas/product.schema.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | import { Document, Types } from 'mongoose'; 3 | 4 | export type ProductDocument = Product & Document; 5 | 6 | @Schema({ timestamps: true }) 7 | export class Review { 8 | @Prop({ required: true }) 9 | userId: string; 10 | 11 | @Prop({ required: true }) 12 | userName: string; 13 | 14 | @Prop({ required: true, min: 1, max: 5 }) 15 | rating: number; 16 | 17 | @Prop({ required: true }) 18 | comment: string; 19 | 20 | @Prop({ default: Date.now }) 21 | createdAt: Date; 22 | } 23 | 24 | @Schema({ timestamps: true }) 25 | export class Product { 26 | @Prop({ required: true }) 27 | name: string; 28 | 29 | @Prop({ required: true }) 30 | description: string; 31 | 32 | @Prop({ required: true, unique: true }) 33 | sku: string; 34 | 35 | @Prop({ required: true, min: 0 }) 36 | price: number; 37 | 38 | @Prop({ min: 0, default: 0 }) 39 | comparePrice?: number; 40 | 41 | @Prop({ required: true, min: 0 }) 42 | stock: number; 43 | 44 | @Prop({ type: Types.ObjectId, ref: 'Category', required: true }) 45 | categoryId: Types.ObjectId; 46 | 47 | @Prop({ type: [String], default: [] }) 48 | images: string[]; 49 | 50 | @Prop({ type: [String], default: [] }) 51 | tags: string[]; 52 | 53 | @Prop({ default: true }) 54 | isActive: boolean; 55 | 56 | @Prop({ default: false }) 57 | isFeatured: boolean; 58 | 59 | @Prop({ type: Object }) 60 | specifications?: Record; 61 | 62 | @Prop({ type: Object }) 63 | seoData?: { 64 | title?: string; 65 | description?: string; 66 | keywords?: string[]; 67 | }; 68 | 69 | @Prop({ type: [Review], default: [] }) 70 | reviews: Review[]; 71 | 72 | @Prop({ default: 0, min: 0, max: 5 }) 73 | averageRating: number; 74 | 75 | @Prop({ default: 0 }) 76 | totalReviews: number; 77 | 78 | @Prop({ default: 0 }) 79 | totalSales: number; 80 | 81 | @Prop({ default: 0 }) 82 | views: number; 83 | } 84 | 85 | export const ProductSchema = SchemaFactory.createForClass(Product); 86 | 87 | // Índices para optimización 88 | ProductSchema.index({ name: 'text', description: 'text' }); 89 | ProductSchema.index({ categoryId: 1 }); 90 | ProductSchema.index({ sku: 1 }); 91 | ProductSchema.index({ price: 1 }); 92 | ProductSchema.index({ isActive: 1 }); 93 | ProductSchema.index({ isFeatured: 1 }); 94 | ProductSchema.index({ averageRating: -1 }); 95 | ProductSchema.index({ totalSales: -1 }); 96 | ProductSchema.index({ createdAt: -1 }); 97 | -------------------------------------------------------------------------------- /microservices/notification-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notification-service", 3 | "version": "1.0.0", 4 | "description": "Microservicio de notificaciones en tiempo real", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json" 21 | }, 22 | "dependencies": { 23 | "@nestjs/common": "^11.0.1", 24 | "@nestjs/core": "^11.0.1", 25 | "@nestjs/platform-express": "^11.0.1", 26 | "@nestjs/platform-socket.io": "^11.0.1", 27 | "@nestjs/websockets": "^11.0.1", 28 | "@nestjs/jwt": "^10.2.0", 29 | "@nestjs/passport": "^10.0.3", 30 | "socket.io": "^4.7.4", 31 | "redis": "^4.6.10", 32 | "ioredis": "^5.3.2", 33 | "passport": "^0.6.0", 34 | "passport-jwt": "^4.0.1", 35 | "class-validator": "^0.14.0", 36 | "class-transformer": "^0.5.1", 37 | "reflect-metadata": "^0.2.2", 38 | "rxjs": "^7.8.1" 39 | }, 40 | "devDependencies": { 41 | "@nestjs/cli": "^11.0.0", 42 | "@nestjs/schematics": "^11.0.0", 43 | "@nestjs/testing": "^11.0.1", 44 | "@types/express": "^5.0.0", 45 | "@types/jest": "^29.5.14", 46 | "@types/node": "^22.10.7", 47 | "@types/passport-jwt": "^3.0.13", 48 | "@types/supertest": "^6.0.2", 49 | "eslint": "^9.18.0", 50 | "jest": "^29.7.0", 51 | "prettier": "^3.4.2", 52 | "source-map-support": "^0.5.21", 53 | "supertest": "^7.0.0", 54 | "ts-jest": "^29.2.5", 55 | "ts-loader": "^9.5.2", 56 | "ts-node": "^10.9.2", 57 | "tsconfig-paths": "^4.2.0", 58 | "typescript": "^5.7.3" 59 | }, 60 | "jest": { 61 | "moduleFileExtensions": [ 62 | "js", 63 | "json", 64 | "ts" 65 | ], 66 | "rootDir": "src", 67 | "testRegex": ".*\\.spec\\.ts$", 68 | "transform": { 69 | "^.+\\.(t|j)s$": "ts-jest" 70 | }, 71 | "collectCoverageFrom": [ 72 | "**/*.(t|j)s" 73 | ], 74 | "coverageDirectory": "../coverage", 75 | "testEnvironment": "node" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /microservices/user-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "user-service", 3 | "version": "1.0.0", 4 | "description": "Microservicio de gestión de usuarios", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json", 21 | "typeorm": "typeorm-ts-node-commonjs" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^11.0.1", 25 | "@nestjs/core": "^11.0.1", 26 | "@nestjs/platform-express": "^11.0.1", 27 | "@nestjs/mapped-types": "^2.0.5", 28 | "@nestjs/typeorm": "^10.0.2", 29 | "@nestjs/jwt": "^10.2.0", 30 | "@nestjs/passport": "^10.0.3", 31 | "typeorm": "^0.3.17", 32 | "pg": "^8.11.3", 33 | "bcryptjs": "^2.4.3", 34 | "passport": "^0.6.0", 35 | "passport-jwt": "^4.0.1", 36 | "class-validator": "^0.14.0", 37 | "class-transformer": "^0.5.1", 38 | "reflect-metadata": "^0.2.2", 39 | "rxjs": "^7.8.1" 40 | }, 41 | "devDependencies": { 42 | "@nestjs/cli": "^11.0.0", 43 | "@nestjs/schematics": "^11.0.0", 44 | "@nestjs/testing": "^11.0.1", 45 | "@types/express": "^5.0.0", 46 | "@types/jest": "^29.5.14", 47 | "@types/node": "^22.10.7", 48 | "@types/pg": "^8.10.9", 49 | "@types/bcryptjs": "^2.4.6", 50 | "@types/passport-jwt": "^3.0.13", 51 | "@types/supertest": "^6.0.2", 52 | "eslint": "^9.18.0", 53 | "jest": "^29.7.0", 54 | "prettier": "^3.4.2", 55 | "source-map-support": "^0.5.21", 56 | "supertest": "^7.0.0", 57 | "ts-jest": "^29.2.5", 58 | "ts-loader": "^9.5.2", 59 | "ts-node": "^10.9.2", 60 | "tsconfig-paths": "^4.2.0", 61 | "typescript": "^5.7.3" 62 | }, 63 | "jest": { 64 | "moduleFileExtensions": [ 65 | "js", 66 | "json", 67 | "ts" 68 | ], 69 | "rootDir": "src", 70 | "testRegex": ".*\\.spec\\.ts$", 71 | "transform": { 72 | "^.+\\.(t|j)s$": "ts-jest" 73 | }, 74 | "collectCoverageFrom": [ 75 | "**/*.(t|j)s" 76 | ], 77 | "coverageDirectory": "../coverage", 78 | "testEnvironment": "node" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | pnpm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | *.lcov 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (https://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # TypeScript v1 declaration files 43 | typings/ 44 | 45 | # TypeScript cache 46 | *.tsbuildinfo 47 | 48 | # Optional npm cache directory 49 | .npm 50 | 51 | # Optional eslint cache 52 | .eslintcache 53 | 54 | # Microbundle cache 55 | .rpt2_cache/ 56 | .rts2_cache_cjs/ 57 | .rts2_cache_es/ 58 | .rts2_cache_umd/ 59 | 60 | # Optional REPL history 61 | .node_repl_history 62 | 63 | # Output of 'npm pack' 64 | *.tgz 65 | 66 | # Yarn Integrity file 67 | .yarn-integrity 68 | 69 | # dotenv environment variables file 70 | .env 71 | .env.test 72 | .env.production 73 | .env.local 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and not Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | # Stores VSCode versions used for testing VSCode extensions 108 | .vscode-test 109 | 110 | # yarn v2 111 | .yarn/cache 112 | .yarn/unplugged 113 | .yarn/build-state.yml 114 | .yarn/install-state.gz 115 | .pnp.* 116 | 117 | # IDEs 118 | .vscode/ 119 | .idea/ 120 | *.swp 121 | *.swo 122 | *~ 123 | 124 | # OS generated files 125 | .DS_Store 126 | .DS_Store? 127 | ._* 128 | .Spotlight-V100 129 | .Trashes 130 | ehthumbs.db 131 | Thumbs.db 132 | 133 | # Docker 134 | .dockerignore 135 | 136 | # Build directories 137 | dist/ 138 | build/ -------------------------------------------------------------------------------- /microservices/api-gateway/src/modules/notification/notification.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; 2 | import { HttpService } from '@nestjs/axios'; 3 | import { firstValueFrom } from 'rxjs'; 4 | 5 | @Injectable() 6 | export class NotificationService { 7 | private readonly notificationServiceUrl = process.env.NOTIFICATION_SERVICE_URL || 'http://localhost:3003'; 8 | 9 | constructor(private readonly httpService: HttpService) {} 10 | 11 | async findAll(userId: string) { 12 | try { 13 | const response = await firstValueFrom( 14 | this.httpService.get(`${this.notificationServiceUrl}/notifications`, { 15 | params: { userId } 16 | }) 17 | ); 18 | return response.data; 19 | } catch (error) { 20 | throw new HttpException( 21 | 'Error al obtener notificaciones del microservicio', 22 | HttpStatus.SERVICE_UNAVAILABLE 23 | ); 24 | } 25 | } 26 | 27 | async send(sendNotificationDto: any) { 28 | try { 29 | const response = await firstValueFrom( 30 | this.httpService.post(`${this.notificationServiceUrl}/notifications/send`, sendNotificationDto) 31 | ); 32 | return response.data; 33 | } catch (error) { 34 | if (error.response?.status === 400) { 35 | throw new HttpException(error.response.data.message, HttpStatus.BAD_REQUEST); 36 | } 37 | throw new HttpException( 38 | 'Error al enviar notificación en el microservicio', 39 | HttpStatus.SERVICE_UNAVAILABLE 40 | ); 41 | } 42 | } 43 | 44 | async markAsRead(id: string, userId: string) { 45 | try { 46 | const response = await firstValueFrom( 47 | this.httpService.post(`${this.notificationServiceUrl}/notifications/${id}/read`, { userId }) 48 | ); 49 | return response.data; 50 | } catch (error) { 51 | if (error.response?.status === 404) { 52 | throw new HttpException('Notificación no encontrada', HttpStatus.NOT_FOUND); 53 | } 54 | throw new HttpException( 55 | 'Error al marcar notificación como leída', 56 | HttpStatus.SERVICE_UNAVAILABLE 57 | ); 58 | } 59 | } 60 | 61 | async getUnreadCount(userId: string) { 62 | try { 63 | const response = await firstValueFrom( 64 | this.httpService.get(`${this.notificationServiceUrl}/notifications/unread-count`, { 65 | params: { userId } 66 | }) 67 | ); 68 | return response.data; 69 | } catch (error) { 70 | throw new HttpException( 71 | 'Error al obtener contador de notificaciones no leídas', 72 | HttpStatus.SERVICE_UNAVAILABLE 73 | ); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /microservices/api-gateway/src/modules/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; 2 | import { HttpService } from '@nestjs/axios'; 3 | import { JwtService } from '@nestjs/jwt'; 4 | import { firstValueFrom } from 'rxjs'; 5 | 6 | @Injectable() 7 | export class AuthService { 8 | private readonly userServiceUrl = process.env.USER_SERVICE_URL || 'http://localhost:3001'; 9 | 10 | constructor( 11 | private readonly httpService: HttpService, 12 | private readonly jwtService: JwtService, 13 | ) {} 14 | 15 | async login(loginDto: { email: string; password: string }) { 16 | try { 17 | const response = await firstValueFrom( 18 | this.httpService.post(`${this.userServiceUrl}/auth/login`, loginDto) 19 | ); 20 | 21 | const user = response.data.user; 22 | const payload = { email: user.email, sub: user.id, roles: user.roles }; 23 | 24 | return { 25 | access_token: this.jwtService.sign(payload), 26 | user: { 27 | id: user.id, 28 | email: user.email, 29 | name: user.name, 30 | roles: user.roles, 31 | }, 32 | }; 33 | } catch (error) { 34 | if (error.response?.status === 401) { 35 | throw new HttpException('Credenciales inválidas', HttpStatus.UNAUTHORIZED); 36 | } 37 | throw new HttpException( 38 | 'Error en el servicio de autenticación', 39 | HttpStatus.SERVICE_UNAVAILABLE 40 | ); 41 | } 42 | } 43 | 44 | async register(registerDto: any) { 45 | try { 46 | const response = await firstValueFrom( 47 | this.httpService.post(`${this.userServiceUrl}/auth/register`, registerDto) 48 | ); 49 | 50 | const user = response.data.user; 51 | const payload = { email: user.email, sub: user.id, roles: user.roles }; 52 | 53 | return { 54 | access_token: this.jwtService.sign(payload), 55 | user: { 56 | id: user.id, 57 | email: user.email, 58 | name: user.name, 59 | roles: user.roles, 60 | }, 61 | }; 62 | } catch (error) { 63 | if (error.response?.status === 400) { 64 | throw new HttpException(error.response.data.message, HttpStatus.BAD_REQUEST); 65 | } 66 | throw new HttpException( 67 | 'Error al registrar usuario', 68 | HttpStatus.SERVICE_UNAVAILABLE 69 | ); 70 | } 71 | } 72 | 73 | async getProfile(userId: string) { 74 | try { 75 | const response = await firstValueFrom( 76 | this.httpService.get(`${this.userServiceUrl}/users/${userId}`) 77 | ); 78 | return response.data; 79 | } catch (error) { 80 | throw new HttpException( 81 | 'Error al obtener perfil de usuario', 82 | HttpStatus.SERVICE_UNAVAILABLE 83 | ); 84 | } 85 | } 86 | 87 | async refreshToken(user: any) { 88 | const payload = { email: user.email, sub: user.id, roles: user.roles }; 89 | return { 90 | access_token: this.jwtService.sign(payload), 91 | }; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /microservices/api-gateway/src/modules/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; 2 | import { HttpService } from '@nestjs/axios'; 3 | import { firstValueFrom } from 'rxjs'; 4 | 5 | @Injectable() 6 | export class UserService { 7 | private readonly userServiceUrl = process.env.USER_SERVICE_URL || 'http://localhost:3001'; 8 | 9 | constructor(private readonly httpService: HttpService) {} 10 | 11 | async findAll(query: any) { 12 | try { 13 | const response = await firstValueFrom( 14 | this.httpService.get(`${this.userServiceUrl}/users`, { params: query }) 15 | ); 16 | return response.data; 17 | } catch (error) { 18 | throw new HttpException( 19 | 'Error al obtener usuarios del microservicio', 20 | HttpStatus.SERVICE_UNAVAILABLE 21 | ); 22 | } 23 | } 24 | 25 | async findOne(id: string) { 26 | try { 27 | const response = await firstValueFrom( 28 | this.httpService.get(`${this.userServiceUrl}/users/${id}`) 29 | ); 30 | return response.data; 31 | } catch (error) { 32 | if (error.response?.status === 404) { 33 | throw new HttpException('Usuario no encontrado', HttpStatus.NOT_FOUND); 34 | } 35 | throw new HttpException( 36 | 'Error al obtener usuario del microservicio', 37 | HttpStatus.SERVICE_UNAVAILABLE 38 | ); 39 | } 40 | } 41 | 42 | async create(createUserDto: any) { 43 | try { 44 | const response = await firstValueFrom( 45 | this.httpService.post(`${this.userServiceUrl}/users`, createUserDto) 46 | ); 47 | return response.data; 48 | } catch (error) { 49 | if (error.response?.status === 400) { 50 | throw new HttpException(error.response.data.message, HttpStatus.BAD_REQUEST); 51 | } 52 | throw new HttpException( 53 | 'Error al crear usuario en el microservicio', 54 | HttpStatus.SERVICE_UNAVAILABLE 55 | ); 56 | } 57 | } 58 | 59 | async update(id: string, updateUserDto: any) { 60 | try { 61 | const response = await firstValueFrom( 62 | this.httpService.put(`${this.userServiceUrl}/users/${id}`, updateUserDto) 63 | ); 64 | return response.data; 65 | } catch (error) { 66 | if (error.response?.status === 404) { 67 | throw new HttpException('Usuario no encontrado', HttpStatus.NOT_FOUND); 68 | } 69 | throw new HttpException( 70 | 'Error al actualizar usuario en el microservicio', 71 | HttpStatus.SERVICE_UNAVAILABLE 72 | ); 73 | } 74 | } 75 | 76 | async remove(id: string) { 77 | try { 78 | const response = await firstValueFrom( 79 | this.httpService.delete(`${this.userServiceUrl}/users/${id}`) 80 | ); 81 | return response.data; 82 | } catch (error) { 83 | if (error.response?.status === 404) { 84 | throw new HttpException('Usuario no encontrado', HttpStatus.NOT_FOUND); 85 | } 86 | throw new HttpException( 87 | 'Error al eliminar usuario del microservicio', 88 | HttpStatus.SERVICE_UNAVAILABLE 89 | ); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /microservices/product-service/src/modules/product/product.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Patch, 7 | Param, 8 | Delete, 9 | Query, 10 | } from '@nestjs/common'; 11 | import { ProductService } from './product.service'; 12 | import { CreateProductDto } from './dto/create-product.dto'; 13 | import { UpdateProductDto } from './dto/update-product.dto'; 14 | import { AddReviewDto } from './dto/add-review.dto'; 15 | 16 | @Controller('products') 17 | export class ProductController { 18 | constructor(private readonly productService: ProductService) {} 19 | 20 | @Post() 21 | create(@Body() createProductDto: CreateProductDto) { 22 | return this.productService.create(createProductDto); 23 | } 24 | 25 | @Get() 26 | findAll( 27 | @Query('page') page: number = 1, 28 | @Query('limit') limit: number = 10, 29 | @Query('search') search?: string, 30 | @Query('category') category?: string, 31 | @Query('minPrice') minPrice?: number, 32 | @Query('maxPrice') maxPrice?: number, 33 | @Query('featured') featured?: boolean, 34 | @Query('active') active?: boolean, 35 | @Query('sortBy') sortBy: string = 'createdAt', 36 | @Query('sortOrder') sortOrder: 'asc' | 'desc' = 'desc', 37 | ) { 38 | return this.productService.findAll({ 39 | page, 40 | limit, 41 | search, 42 | category, 43 | minPrice, 44 | maxPrice, 45 | featured, 46 | active, 47 | sortBy, 48 | sortOrder, 49 | }); 50 | } 51 | 52 | @Get(':id') 53 | findOne(@Param('id') id: string) { 54 | return this.productService.findOne(id); 55 | } 56 | 57 | @Get('sku/:sku') 58 | findBySku(@Param('sku') sku: string) { 59 | return this.productService.findBySku(sku); 60 | } 61 | 62 | @Get('category/:categoryId') 63 | findByCategory( 64 | @Param('categoryId') categoryId: string, 65 | @Query('page') page: number = 1, 66 | @Query('limit') limit: number = 10, 67 | ) { 68 | return this.productService.findByCategory(categoryId, page, limit); 69 | } 70 | 71 | @Patch(':id') 72 | update(@Param('id') id: string, @Body() updateProductDto: UpdateProductDto) { 73 | return this.productService.update(id, updateProductDto); 74 | } 75 | 76 | @Delete(':id') 77 | remove(@Param('id') id: string) { 78 | return this.productService.remove(id); 79 | } 80 | 81 | @Post(':id/reviews') 82 | addReview(@Param('id') id: string, @Body() addReviewDto: AddReviewDto) { 83 | return this.productService.addReview(id, addReviewDto); 84 | } 85 | 86 | @Patch(':id/stock') 87 | updateStock(@Param('id') id: string, @Body('quantity') quantity: number) { 88 | return this.productService.updateStock(id, quantity); 89 | } 90 | 91 | @Patch(':id/view') 92 | incrementViews(@Param('id') id: string) { 93 | return this.productService.incrementViews(id); 94 | } 95 | 96 | @Get('featured/list') 97 | getFeatured(@Query('limit') limit: number = 10) { 98 | return this.productService.getFeatured(limit); 99 | } 100 | 101 | @Get('popular/list') 102 | getPopular(@Query('limit') limit: number = 10) { 103 | return this.productService.getPopular(limit); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /microservices/user-service/src/modules/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | UnauthorizedException, 4 | ConflictException, 5 | } from '@nestjs/common'; 6 | import { JwtService } from '@nestjs/jwt'; 7 | import { UserService } from '../user/user.service'; 8 | import { CreateUserDto } from '../user/dto/create-user.dto'; 9 | import { LoginDto } from '../user/dto/login.dto'; 10 | import { User } from '../user/entities/user.entity'; 11 | 12 | @Injectable() 13 | export class AuthService { 14 | constructor( 15 | private readonly userService: UserService, 16 | private readonly jwtService: JwtService, 17 | ) {} 18 | 19 | async register(createUserDto: CreateUserDto) { 20 | try { 21 | const user = await this.userService.create(createUserDto); 22 | const payload = { email: user.email, sub: user.id, roles: user.roles }; 23 | 24 | return { 25 | message: 'Usuario registrado exitosamente', 26 | user: { 27 | id: user.id, 28 | email: user.email, 29 | name: user.name, 30 | lastName: user.lastName, 31 | roles: user.roles, 32 | isActive: user.isActive, 33 | createdAt: user.createdAt, 34 | }, 35 | access_token: this.jwtService.sign(payload), 36 | }; 37 | } catch (error) { 38 | if (error instanceof ConflictException) { 39 | throw error; 40 | } 41 | throw new ConflictException('Error al registrar usuario'); 42 | } 43 | } 44 | 45 | async login(loginDto: LoginDto) { 46 | const user = await this.validateUser(loginDto.email, loginDto.password); 47 | 48 | if (!user.isActive) { 49 | throw new UnauthorizedException('Usuario desactivado'); 50 | } 51 | 52 | // Actualizar último login 53 | await this.userService.updateLastLogin(user.id); 54 | 55 | const payload = { email: user.email, sub: user.id, roles: user.roles }; 56 | 57 | return { 58 | message: 'Login exitoso', 59 | user: { 60 | id: user.id, 61 | email: user.email, 62 | name: user.name, 63 | lastName: user.lastName, 64 | roles: user.roles, 65 | isActive: user.isActive, 66 | lastLogin: new Date(), 67 | }, 68 | access_token: this.jwtService.sign(payload), 69 | }; 70 | } 71 | 72 | async validateUser(email: string, password: string): Promise { 73 | try { 74 | const user = await this.userService.findByEmail(email); 75 | 76 | if (user && (await user.validatePassword(password))) { 77 | return user; 78 | } 79 | 80 | throw new UnauthorizedException('Credenciales inválidas'); 81 | } catch (error) { 82 | throw new UnauthorizedException('Credenciales inválidas'); 83 | } 84 | } 85 | 86 | async validateToken(token: string) { 87 | try { 88 | const payload = this.jwtService.verify(token); 89 | const user = await this.userService.findOne(payload.sub); 90 | 91 | return { 92 | valid: true, 93 | user: { 94 | id: user.id, 95 | email: user.email, 96 | name: user.name, 97 | roles: user.roles, 98 | isActive: user.isActive, 99 | }, 100 | }; 101 | } catch (error) { 102 | return { 103 | valid: false, 104 | message: 'Token inválido o expirado', 105 | }; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | # Bases de datos 3 | postgres: 4 | image: postgres:15 5 | container_name: microservices-postgres 6 | environment: 7 | POSTGRES_DB: userdb 8 | POSTGRES_USER: postgres 9 | POSTGRES_PASSWORD: postgres123 10 | ports: 11 | - '5432:5432' 12 | volumes: 13 | - postgres_data:/var/lib/postgresql/data 14 | networks: 15 | - microservices-network 16 | 17 | mongodb: 18 | image: mongo:7 19 | container_name: microservices-mongo 20 | environment: 21 | MONGO_INITDB_ROOT_USERNAME: mongo 22 | MONGO_INITDB_ROOT_PASSWORD: mongo123 23 | MONGO_INITDB_DATABASE: productdb 24 | ports: 25 | - '28017:27017' 26 | volumes: 27 | - mongodb_data:/data/db 28 | networks: 29 | - microservices-network 30 | 31 | redis: 32 | image: redis:7-alpine 33 | container_name: microservices-redis 34 | ports: 35 | - '6379:6379' 36 | command: redis-server --appendonly yes --requirepass redis123 37 | volumes: 38 | - redis_data:/data 39 | networks: 40 | - microservices-network 41 | 42 | # API Gateway 43 | api-gateway: 44 | build: 45 | context: ./microservices/api-gateway 46 | dockerfile: Dockerfile 47 | container_name: api-gateway 48 | ports: 49 | - '3000:3000' 50 | environment: 51 | - NODE_ENV=development 52 | - USER_SERVICE_URL=http://user-service:3001 53 | - PRODUCT_SERVICE_URL=http://product-service:3002 54 | - NOTIFICATION_SERVICE_URL=http://notification-service:3003 55 | depends_on: 56 | - user-service 57 | - product-service 58 | - notification-service 59 | networks: 60 | - microservices-network 61 | volumes: 62 | - ./microservices/api-gateway:/app 63 | - /app/node_modules 64 | command: npm run start:dev 65 | 66 | # User Service 67 | user-service: 68 | build: 69 | context: ./microservices/user-service 70 | dockerfile: Dockerfile 71 | container_name: user-service 72 | ports: 73 | - '3001:3001' 74 | environment: 75 | - NODE_ENV=development 76 | - DATABASE_HOST=postgres 77 | - DATABASE_PORT=5432 78 | - DATABASE_NAME=userdb 79 | - DATABASE_USER=postgres 80 | - DATABASE_PASSWORD=postgres123 81 | - JWT_SECRET=your-super-secret-jwt-key 82 | depends_on: 83 | - postgres 84 | networks: 85 | - microservices-network 86 | volumes: 87 | - ./microservices/user-service:/app 88 | - /app/node_modules 89 | command: npm run start:dev 90 | 91 | # Product Service 92 | product-service: 93 | build: 94 | context: ./microservices/product-service 95 | dockerfile: Dockerfile 96 | container_name: product-service 97 | ports: 98 | - '3002:3002' 99 | environment: 100 | - NODE_ENV=development 101 | - MONGODB_URI=mongodb://mongo:mongo123@mongodb:27017/productdb?authSource=admin 102 | depends_on: 103 | - mongodb 104 | networks: 105 | - microservices-network 106 | volumes: 107 | - ./microservices/product-service:/app 108 | - /app/node_modules 109 | command: npm run start:dev 110 | 111 | # Notification Service 112 | notification-service: 113 | build: 114 | context: ./microservices/notification-service 115 | dockerfile: Dockerfile 116 | container_name: notification-service 117 | ports: 118 | - '3003:3003' 119 | environment: 120 | - NODE_ENV=development 121 | - REDIS_HOST=redis 122 | - REDIS_PORT=6379 123 | - REDIS_PASSWORD=redis123 124 | depends_on: 125 | - redis 126 | networks: 127 | - microservices-network 128 | volumes: 129 | - ./microservices/notification-service:/app 130 | - /app/node_modules 131 | command: npm run start:dev 132 | 133 | volumes: 134 | postgres_data: 135 | mongodb_data: 136 | redis_data: 137 | 138 | networks: 139 | microservices-network: 140 | driver: bridge 141 | -------------------------------------------------------------------------------- /microservices/user-service/src/modules/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | NotFoundException, 4 | ConflictException, 5 | BadRequestException, 6 | } from '@nestjs/common'; 7 | import { InjectRepository } from '@nestjs/typeorm'; 8 | import { Repository, Like } from 'typeorm'; 9 | import { CreateUserDto } from './dto/create-user.dto'; 10 | import { UpdateUserDto } from './dto/update-user.dto'; 11 | import { User } from './entities/user.entity'; 12 | 13 | @Injectable() 14 | export class UserService { 15 | constructor( 16 | @InjectRepository(User) 17 | private userRepository: Repository, 18 | ) {} 19 | 20 | async create(createUserDto: CreateUserDto): Promise { 21 | // Verificar si el email ya existe 22 | const existingUser = await this.userRepository.findOne({ 23 | where: { email: createUserDto.email }, 24 | }); 25 | 26 | if (existingUser) { 27 | throw new ConflictException('El email ya está registrado'); 28 | } 29 | 30 | const user = this.userRepository.create({ 31 | ...createUserDto, 32 | roles: createUserDto.roles || ['user'], 33 | }); 34 | 35 | return await this.userRepository.save(user); 36 | } 37 | 38 | async findAll(page: number = 1, limit: number = 10, search?: string) { 39 | const skip = (page - 1) * limit; 40 | const where = search 41 | ? [ 42 | { name: Like(`%${search}%`) }, 43 | { lastName: Like(`%${search}%`) }, 44 | { email: Like(`%${search}%`) }, 45 | ] 46 | : {}; 47 | 48 | const [users, total] = await this.userRepository.findAndCount({ 49 | where, 50 | skip, 51 | take: limit, 52 | order: { createdAt: 'DESC' }, 53 | }); 54 | 55 | return { 56 | data: users, 57 | total, 58 | page, 59 | limit, 60 | totalPages: Math.ceil(total / limit), 61 | }; 62 | } 63 | 64 | async findOne(id: string): Promise { 65 | const user = await this.userRepository.findOne({ 66 | where: { id }, 67 | }); 68 | 69 | if (!user) { 70 | throw new NotFoundException('Usuario no encontrado'); 71 | } 72 | 73 | return user; 74 | } 75 | 76 | async findByEmail(email: string): Promise { 77 | const user = await this.userRepository.findOne({ 78 | where: { email }, 79 | }); 80 | 81 | if (!user) { 82 | throw new NotFoundException('Usuario no encontrado'); 83 | } 84 | 85 | return user; 86 | } 87 | 88 | async update(id: string, updateUserDto: UpdateUserDto): Promise { 89 | const user = await this.findOne(id); 90 | 91 | // Si se está actualizando el email, verificar que no exista 92 | if (updateUserDto.email && updateUserDto.email !== user.email) { 93 | const existingUser = await this.userRepository.findOne({ 94 | where: { email: updateUserDto.email }, 95 | }); 96 | 97 | if (existingUser) { 98 | throw new ConflictException('El email ya está registrado'); 99 | } 100 | } 101 | 102 | // No permitir actualización directa de la contraseña aquí 103 | if (updateUserDto.password) { 104 | delete updateUserDto.password; 105 | } 106 | 107 | Object.assign(user, updateUserDto); 108 | return await this.userRepository.save(user); 109 | } 110 | 111 | async remove(id: string): Promise { 112 | const user = await this.findOne(id); 113 | await this.userRepository.remove(user); 114 | } 115 | 116 | async activate(id: string): Promise { 117 | const user = await this.findOne(id); 118 | user.isActive = true; 119 | return await this.userRepository.save(user); 120 | } 121 | 122 | async deactivate(id: string): Promise { 123 | const user = await this.findOne(id); 124 | user.isActive = false; 125 | return await this.userRepository.save(user); 126 | } 127 | 128 | async updateLastLogin(id: string): Promise { 129 | await this.userRepository.update(id, { 130 | lastLogin: new Date(), 131 | }); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /microservices/api-gateway/src/modules/product/product.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; 2 | import { HttpService } from '@nestjs/axios'; 3 | import { firstValueFrom } from 'rxjs'; 4 | 5 | @Injectable() 6 | export class ProductService { 7 | private readonly productServiceUrl = process.env.PRODUCT_SERVICE_URL || 'http://localhost:3002'; 8 | 9 | constructor(private readonly httpService: HttpService) {} 10 | 11 | async findAll(query: any) { 12 | try { 13 | const response = await firstValueFrom( 14 | this.httpService.get(`${this.productServiceUrl}/products`, { params: query }) 15 | ); 16 | return response.data; 17 | } catch (error) { 18 | throw new HttpException( 19 | 'Error al obtener productos del microservicio', 20 | HttpStatus.SERVICE_UNAVAILABLE 21 | ); 22 | } 23 | } 24 | 25 | async findOne(id: string) { 26 | try { 27 | const response = await firstValueFrom( 28 | this.httpService.get(`${this.productServiceUrl}/products/${id}`) 29 | ); 30 | return response.data; 31 | } catch (error) { 32 | if (error.response?.status === 404) { 33 | throw new HttpException('Producto no encontrado', HttpStatus.NOT_FOUND); 34 | } 35 | throw new HttpException( 36 | 'Error al obtener producto del microservicio', 37 | HttpStatus.SERVICE_UNAVAILABLE 38 | ); 39 | } 40 | } 41 | 42 | async create(createProductDto: any) { 43 | try { 44 | const response = await firstValueFrom( 45 | this.httpService.post(`${this.productServiceUrl}/products`, createProductDto) 46 | ); 47 | return response.data; 48 | } catch (error) { 49 | if (error.response?.status === 400) { 50 | throw new HttpException(error.response.data.message, HttpStatus.BAD_REQUEST); 51 | } 52 | throw new HttpException( 53 | 'Error al crear producto en el microservicio', 54 | HttpStatus.SERVICE_UNAVAILABLE 55 | ); 56 | } 57 | } 58 | 59 | async update(id: string, updateProductDto: any) { 60 | try { 61 | const response = await firstValueFrom( 62 | this.httpService.put(`${this.productServiceUrl}/products/${id}`, updateProductDto) 63 | ); 64 | return response.data; 65 | } catch (error) { 66 | if (error.response?.status === 404) { 67 | throw new HttpException('Producto no encontrado', HttpStatus.NOT_FOUND); 68 | } 69 | throw new HttpException( 70 | 'Error al actualizar producto en el microservicio', 71 | HttpStatus.SERVICE_UNAVAILABLE 72 | ); 73 | } 74 | } 75 | 76 | async remove(id: string) { 77 | try { 78 | const response = await firstValueFrom( 79 | this.httpService.delete(`${this.productServiceUrl}/products/${id}`) 80 | ); 81 | return response.data; 82 | } catch (error) { 83 | if (error.response?.status === 404) { 84 | throw new HttpException('Producto no encontrado', HttpStatus.NOT_FOUND); 85 | } 86 | throw new HttpException( 87 | 'Error al eliminar producto del microservicio', 88 | HttpStatus.SERVICE_UNAVAILABLE 89 | ); 90 | } 91 | } 92 | 93 | async findByCategory(category: string, query: any) { 94 | try { 95 | const response = await firstValueFrom( 96 | this.httpService.get(`${this.productServiceUrl}/products/category/${category}`, { params: query }) 97 | ); 98 | return response.data; 99 | } catch (error) { 100 | throw new HttpException( 101 | 'Error al obtener productos por categoría del microservicio', 102 | HttpStatus.SERVICE_UNAVAILABLE 103 | ); 104 | } 105 | } 106 | 107 | async addReview(id: string, reviewDto: any) { 108 | try { 109 | const response = await firstValueFrom( 110 | this.httpService.post(`${this.productServiceUrl}/products/${id}/reviews`, reviewDto) 111 | ); 112 | return response.data; 113 | } catch (error) { 114 | if (error.response?.status === 404) { 115 | throw new HttpException('Producto no encontrado', HttpStatus.NOT_FOUND); 116 | } 117 | throw new HttpException( 118 | 'Error al agregar reseña en el microservicio', 119 | HttpStatus.SERVICE_UNAVAILABLE 120 | ); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /microservices/notification-service/src/modules/redis/redis.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common'; 2 | import { Redis } from 'ioredis'; 3 | 4 | @Injectable() 5 | export class RedisService implements OnModuleInit, OnModuleDestroy { 6 | private readonly logger = new Logger(RedisService.name); 7 | private client: Redis; 8 | private subscriber: Redis; 9 | private publisher: Redis; 10 | 11 | async onModuleInit() { 12 | const redisConfig = { 13 | host: process.env.REDIS_HOST || 'localhost', 14 | port: parseInt(process.env.REDIS_PORT || '6379'), 15 | password: process.env.REDIS_PASSWORD || 'redis123', 16 | retryDelayOnFailover: 100, 17 | maxRetriesPerRequest: 3, 18 | }; 19 | 20 | try { 21 | // Cliente principal para operaciones generales 22 | this.client = new Redis(redisConfig); 23 | 24 | // Cliente para suscripciones 25 | this.subscriber = new Redis(redisConfig); 26 | 27 | // Cliente para publicaciones 28 | this.publisher = new Redis(redisConfig); 29 | 30 | this.client.on('connect', () => { 31 | this.logger.log('Conectado a Redis'); 32 | }); 33 | 34 | this.client.on('error', (error) => { 35 | this.logger.error('Error de conexión a Redis:', error); 36 | }); 37 | 38 | await this.client.ping(); 39 | this.logger.log('Redis conectado exitosamente'); 40 | } catch (error) { 41 | this.logger.error('Error al conectar con Redis:', error); 42 | throw error; 43 | } 44 | } 45 | 46 | async onModuleDestroy() { 47 | if (this.client) { 48 | await this.client.quit(); 49 | } 50 | if (this.subscriber) { 51 | await this.subscriber.quit(); 52 | } 53 | if (this.publisher) { 54 | await this.publisher.quit(); 55 | } 56 | this.logger.log('Conexiones Redis cerradas'); 57 | } 58 | 59 | getClient(): Redis { 60 | return this.client; 61 | } 62 | 63 | getSubscriber(): Redis { 64 | return this.subscriber; 65 | } 66 | 67 | getPublisher(): Redis { 68 | return this.publisher; 69 | } 70 | 71 | // Métodos de utilidad para notificaciones 72 | async saveNotification(userId: string, notification: any): Promise { 73 | const key = `notifications:${userId}`; 74 | const notificationWithId = { 75 | ...notification, 76 | id: `notif_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, 77 | timestamp: new Date().toISOString(), 78 | read: false, 79 | }; 80 | 81 | await this.client.lpush(key, JSON.stringify(notificationWithId)); 82 | 83 | // Mantener solo las últimas 100 notificaciones por usuario 84 | await this.client.ltrim(key, 0, 99); 85 | } 86 | 87 | async getNotifications(userId: string, offset = 0, limit = 20): Promise { 88 | const key = `notifications:${userId}`; 89 | const notifications = await this.client.lrange(key, offset, offset + limit - 1); 90 | return notifications.map(notif => JSON.parse(notif)); 91 | } 92 | 93 | async markNotificationAsRead(userId: string, notificationId: string): Promise { 94 | const key = `notifications:${userId}`; 95 | const notifications = await this.client.lrange(key, 0, -1); 96 | 97 | for (let i = 0; i < notifications.length; i++) { 98 | const notif = JSON.parse(notifications[i]); 99 | if (notif.id === notificationId) { 100 | notif.read = true; 101 | await this.client.lset(key, i, JSON.stringify(notif)); 102 | return true; 103 | } 104 | } 105 | 106 | return false; 107 | } 108 | 109 | async getUnreadCount(userId: string): Promise { 110 | const notifications = await this.getNotifications(userId, 0, 100); 111 | return notifications.filter(notif => !notif.read).length; 112 | } 113 | 114 | async publishNotification(channel: string, data: any): Promise { 115 | await this.publisher.publish(channel, JSON.stringify(data)); 116 | } 117 | 118 | async subscribeToChannel(channel: string, callback: (message: string) => void): Promise { 119 | this.subscriber.subscribe(channel); 120 | this.subscriber.on('message', (receivedChannel, message) => { 121 | if (receivedChannel === channel) { 122 | callback(message); 123 | } 124 | }); 125 | } 126 | 127 | // Métodos para manejar usuarios conectados 128 | async addConnectedUser(userId: string, socketId: string): Promise { 129 | await this.client.hset('connected_users', userId, socketId); 130 | } 131 | 132 | async removeConnectedUser(userId: string): Promise { 133 | await this.client.hdel('connected_users', userId); 134 | } 135 | 136 | async getConnectedUser(userId: string): Promise { 137 | return await this.client.hget('connected_users', userId); 138 | } 139 | 140 | async getAllConnectedUsers(): Promise> { 141 | return await this.client.hgetall('connected_users'); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /microservices/product-service/src/modules/category/category.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | NotFoundException, 4 | ConflictException, 5 | } from '@nestjs/common'; 6 | import { InjectModel } from '@nestjs/mongoose'; 7 | import { Model } from 'mongoose'; 8 | import { CreateCategoryDto } from './dto/create-category.dto'; 9 | import { UpdateCategoryDto } from './dto/update-category.dto'; 10 | import { Category, CategoryDocument } from './schemas/category.schema'; 11 | 12 | @Injectable() 13 | export class CategoryService { 14 | constructor( 15 | @InjectModel(Category.name) 16 | private categoryModel: Model, 17 | ) {} 18 | 19 | async create(createCategoryDto: CreateCategoryDto): Promise { 20 | // Verificar si el nombre ya existe 21 | const existingByName = await this.categoryModel.findOne({ 22 | name: createCategoryDto.name, 23 | }); 24 | 25 | if (existingByName) { 26 | throw new ConflictException('Ya existe una categoría con ese nombre'); 27 | } 28 | 29 | // Verificar si el slug ya existe 30 | const existingBySlug = await this.categoryModel.findOne({ 31 | slug: createCategoryDto.slug, 32 | }); 33 | 34 | if (existingBySlug) { 35 | throw new ConflictException('Ya existe una categoría con ese slug'); 36 | } 37 | 38 | const category = new this.categoryModel(createCategoryDto); 39 | return category.save(); 40 | } 41 | 42 | async findAll(page: number = 1, limit: number = 10, search?: string, active?: boolean) { 43 | const skip = (page - 1) * limit; 44 | const filter: any = {}; 45 | 46 | if (search) { 47 | filter.$or = [ 48 | { name: { $regex: search, $options: 'i' } }, 49 | { description: { $regex: search, $options: 'i' } }, 50 | ]; 51 | } 52 | 53 | if (active !== undefined) { 54 | filter.isActive = active; 55 | } 56 | 57 | const [categories, total] = await Promise.all([ 58 | this.categoryModel 59 | .find(filter) 60 | .skip(skip) 61 | .limit(limit) 62 | .sort({ createdAt: -1 }) 63 | .exec(), 64 | this.categoryModel.countDocuments(filter), 65 | ]); 66 | 67 | return { 68 | data: categories, 69 | total, 70 | page, 71 | limit, 72 | totalPages: Math.ceil(total / limit), 73 | }; 74 | } 75 | 76 | async findOne(id: string): Promise { 77 | const category = await this.categoryModel.findById(id).exec(); 78 | 79 | if (!category) { 80 | throw new NotFoundException('Categoría no encontrada'); 81 | } 82 | 83 | return category; 84 | } 85 | 86 | async findBySlug(slug: string): Promise { 87 | const category = await this.categoryModel.findOne({ slug }).exec(); 88 | 89 | if (!category) { 90 | throw new NotFoundException('Categoría no encontrada'); 91 | } 92 | 93 | return category; 94 | } 95 | 96 | async update(id: string, updateCategoryDto: UpdateCategoryDto): Promise { 97 | const category = await this.findOne(id); 98 | 99 | // Si se está actualizando el nombre, verificar que no exista 100 | if (updateCategoryDto.name && updateCategoryDto.name !== category.name) { 101 | const existingByName = await this.categoryModel.findOne({ 102 | name: updateCategoryDto.name, 103 | _id: { $ne: id }, 104 | }); 105 | 106 | if (existingByName) { 107 | throw new ConflictException('Ya existe una categoría con ese nombre'); 108 | } 109 | } 110 | 111 | // Si se está actualizando el slug, verificar que no exista 112 | if (updateCategoryDto.slug && updateCategoryDto.slug !== category.slug) { 113 | const existingBySlug = await this.categoryModel.findOne({ 114 | slug: updateCategoryDto.slug, 115 | _id: { $ne: id }, 116 | }); 117 | 118 | if (existingBySlug) { 119 | throw new ConflictException('Ya existe una categoría con ese slug'); 120 | } 121 | } 122 | 123 | const updatedCategory = await this.categoryModel 124 | .findByIdAndUpdate(id, updateCategoryDto, { new: true }) 125 | .exec(); 126 | 127 | if (!updatedCategory) { 128 | throw new NotFoundException('Categoría no encontrada'); 129 | } 130 | 131 | return updatedCategory; 132 | } 133 | 134 | async remove(id: string): Promise { 135 | const category = await this.findOne(id); 136 | await this.categoryModel.findByIdAndDelete(id).exec(); 137 | } 138 | 139 | async activate(id: string): Promise { 140 | const category = await this.categoryModel 141 | .findByIdAndUpdate(id, { isActive: true }, { new: true }) 142 | .exec(); 143 | 144 | if (!category) { 145 | throw new NotFoundException('Categoría no encontrada'); 146 | } 147 | 148 | return category; 149 | } 150 | 151 | async deactivate(id: string): Promise { 152 | const category = await this.categoryModel 153 | .findByIdAndUpdate(id, { isActive: false }, { new: true }) 154 | .exec(); 155 | 156 | if (!category) { 157 | throw new NotFoundException('Categoría no encontrada'); 158 | } 159 | 160 | return category; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /microservices/notification-service/src/modules/websocket/notifications.gateway.ts: -------------------------------------------------------------------------------- 1 | import { 2 | WebSocketGateway, 3 | WebSocketServer, 4 | SubscribeMessage, 5 | OnGatewayConnection, 6 | OnGatewayDisconnect, 7 | ConnectedSocket, 8 | MessageBody, 9 | } from '@nestjs/websockets'; 10 | import { Logger, UseGuards } from '@nestjs/common'; 11 | import { Server, Socket } from 'socket.io'; 12 | import { RedisService } from '../redis/redis.service'; 13 | import { INotification } from '../notification/interfaces/notification.interface'; 14 | 15 | @WebSocketGateway({ 16 | cors: { 17 | origin: '*', 18 | credentials: true, 19 | }, 20 | namespace: 'notifications', 21 | }) 22 | export class NotificationsGateway implements OnGatewayConnection, OnGatewayDisconnect { 23 | @WebSocketServer() 24 | server: Server; 25 | 26 | private readonly logger = new Logger(NotificationsGateway.name); 27 | 28 | constructor(private readonly redisService: RedisService) {} 29 | 30 | async handleConnection(client: Socket) { 31 | try { 32 | // Extraer el userId de los headers o query params 33 | const userId = client.handshake.query.userId as string; 34 | 35 | if (!userId) { 36 | this.logger.warn(`Cliente desconectado: falta userId`); 37 | client.disconnect(); 38 | return; 39 | } 40 | 41 | // Registrar el usuario conectado en Redis 42 | await this.redisService.addConnectedUser(userId, client.id); 43 | 44 | // Unir al cliente a una sala personal 45 | await client.join(`user_${userId}`); 46 | 47 | this.logger.log(`Usuario ${userId} conectado con socket ${client.id}`); 48 | 49 | // Enviar notificaciones pendientes al usuario recién conectado 50 | await this.sendPendingNotifications(userId); 51 | 52 | // Emitir evento de conexión exitosa 53 | client.emit('connected', { 54 | message: 'Conectado al servicio de notificaciones', 55 | userId, 56 | timestamp: new Date().toISOString(), 57 | }); 58 | 59 | } catch (error) { 60 | this.logger.error('Error en conexión WebSocket:', error); 61 | client.disconnect(); 62 | } 63 | } 64 | 65 | async handleDisconnect(client: Socket) { 66 | try { 67 | const userId = client.handshake.query.userId as string; 68 | 69 | if (userId) { 70 | // Remover usuario de Redis 71 | await this.redisService.removeConnectedUser(userId); 72 | this.logger.log(`Usuario ${userId} desconectado`); 73 | } 74 | } catch (error) { 75 | this.logger.error('Error al desconectar:', error); 76 | } 77 | } 78 | 79 | @SubscribeMessage('join_room') 80 | async handleJoinRoom( 81 | @ConnectedSocket() client: Socket, 82 | @MessageBody() data: { room: string }, 83 | ) { 84 | await client.join(data.room); 85 | client.emit('joined_room', { room: data.room }); 86 | this.logger.log(`Cliente ${client.id} se unió a la sala ${data.room}`); 87 | } 88 | 89 | @SubscribeMessage('leave_room') 90 | async handleLeaveRoom( 91 | @ConnectedSocket() client: Socket, 92 | @MessageBody() data: { room: string }, 93 | ) { 94 | await client.leave(data.room); 95 | client.emit('left_room', { room: data.room }); 96 | this.logger.log(`Cliente ${client.id} salió de la sala ${data.room}`); 97 | } 98 | 99 | @SubscribeMessage('get_notifications') 100 | async handleGetNotifications( 101 | @ConnectedSocket() client: Socket, 102 | @MessageBody() data: { offset?: number; limit?: number }, 103 | ) { 104 | try { 105 | const userId = client.handshake.query.userId as string; 106 | const { offset = 0, limit = 20 } = data; 107 | 108 | const notifications = await this.redisService.getNotifications(userId, offset, limit); 109 | const unreadCount = await this.redisService.getUnreadCount(userId); 110 | 111 | client.emit('notifications_list', { 112 | notifications, 113 | unreadCount, 114 | offset, 115 | limit, 116 | }); 117 | } catch (error) { 118 | this.logger.error('Error al obtener notificaciones:', error); 119 | client.emit('error', { message: 'Error al obtener notificaciones' }); 120 | } 121 | } 122 | 123 | @SubscribeMessage('mark_as_read') 124 | async handleMarkAsRead( 125 | @ConnectedSocket() client: Socket, 126 | @MessageBody() data: { notificationId: string }, 127 | ) { 128 | try { 129 | const userId = client.handshake.query.userId as string; 130 | const success = await this.redisService.markNotificationAsRead(userId, data.notificationId); 131 | 132 | if (success) { 133 | const unreadCount = await this.redisService.getUnreadCount(userId); 134 | client.emit('notification_read', { 135 | notificationId: data.notificationId, 136 | unreadCount, 137 | }); 138 | } else { 139 | client.emit('error', { message: 'Notificación no encontrada' }); 140 | } 141 | } catch (error) { 142 | this.logger.error('Error al marcar como leída:', error); 143 | client.emit('error', { message: 'Error al marcar notificación como leída' }); 144 | } 145 | } 146 | 147 | // Método para enviar notificación a un usuario específico 148 | async sendNotificationToUser(userId: string, notification: INotification) { 149 | try { 150 | // Guardar en Redis 151 | await this.redisService.saveNotification(userId, notification); 152 | 153 | // Verificar si el usuario está conectado 154 | const socketId = await this.redisService.getConnectedUser(userId); 155 | 156 | if (socketId) { 157 | // Enviar notificación en tiempo real 158 | this.server.to(`user_${userId}`).emit('new_notification', notification); 159 | 160 | // Enviar contador actualizado 161 | const unreadCount = await this.redisService.getUnreadCount(userId); 162 | this.server.to(`user_${userId}`).emit('unread_count', { count: unreadCount }); 163 | 164 | this.logger.log(`Notificación enviada a usuario ${userId}`); 165 | } else { 166 | this.logger.log(`Usuario ${userId} no conectado, notificación guardada para más tarde`); 167 | } 168 | } catch (error) { 169 | this.logger.error('Error al enviar notificación:', error); 170 | } 171 | } 172 | 173 | // Método para broadcast a todos los usuarios conectados 174 | async broadcastNotification(notification: Omit) { 175 | try { 176 | const connectedUsers = await this.redisService.getAllConnectedUsers(); 177 | 178 | for (const userId of Object.keys(connectedUsers)) { 179 | const userNotification: INotification = { 180 | ...notification, 181 | userId, 182 | }; 183 | 184 | await this.sendNotificationToUser(userId, userNotification); 185 | } 186 | 187 | this.logger.log(`Broadcast enviado a ${Object.keys(connectedUsers).length} usuarios`); 188 | } catch (error) { 189 | this.logger.error('Error en broadcast:', error); 190 | } 191 | } 192 | 193 | // Método para enviar notificaciones pendientes cuando un usuario se conecta 194 | private async sendPendingNotifications(userId: string) { 195 | try { 196 | const notifications = await this.redisService.getNotifications(userId, 0, 10); 197 | const unreadCount = await this.redisService.getUnreadCount(userId); 198 | 199 | if (notifications.length > 0) { 200 | this.server.to(`user_${userId}`).emit('pending_notifications', { 201 | notifications, 202 | unreadCount, 203 | }); 204 | } 205 | } catch (error) { 206 | this.logger.error('Error al enviar notificaciones pendientes:', error); 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /microservices/notification-service/src/modules/notification/notification.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { RedisService } from '../redis/redis.service'; 3 | import { NotificationsGateway } from '../websocket/notifications.gateway'; 4 | import { SendNotificationDto } from './dto/send-notification.dto'; 5 | import { INotification } from './interfaces/notification.interface'; 6 | 7 | @Injectable() 8 | export class NotificationService { 9 | private readonly logger = new Logger(NotificationService.name); 10 | 11 | constructor( 12 | private readonly redisService: RedisService, 13 | private readonly notificationsGateway: NotificationsGateway, 14 | ) {} 15 | 16 | async sendNotification(sendNotificationDto: SendNotificationDto) { 17 | try { 18 | const { userId, userIds, broadcast, ...notificationData } = sendNotificationDto; 19 | 20 | if (broadcast) { 21 | // Enviar a todos los usuarios conectados 22 | await this.notificationsGateway.broadcastNotification(notificationData); 23 | return { 24 | success: true, 25 | message: 'Notificación broadcast enviada exitosamente', 26 | type: 'broadcast', 27 | }; 28 | } 29 | 30 | if (userIds && userIds.length > 0) { 31 | // Enviar a múltiples usuarios específicos 32 | const results = await Promise.allSettled( 33 | userIds.map(async (id) => { 34 | const notification: INotification = { 35 | ...notificationData, 36 | userId: id, 37 | }; 38 | return this.notificationsGateway.sendNotificationToUser(id, notification); 39 | }), 40 | ); 41 | 42 | const successful = results.filter((result) => result.status === 'fulfilled').length; 43 | const failed = results.length - successful; 44 | 45 | return { 46 | success: true, 47 | message: `Notificaciones enviadas: ${successful} exitosas, ${failed} fallidas`, 48 | type: 'multiple', 49 | stats: { successful, failed, total: results.length }, 50 | }; 51 | } 52 | 53 | if (userId) { 54 | // Enviar a un usuario específico 55 | const notification: INotification = { 56 | ...notificationData, 57 | userId, 58 | }; 59 | 60 | await this.notificationsGateway.sendNotificationToUser(userId, notification); 61 | 62 | return { 63 | success: true, 64 | message: 'Notificación enviada exitosamente', 65 | type: 'single', 66 | userId, 67 | }; 68 | } 69 | 70 | return { 71 | success: false, 72 | message: 'Debe especificar userId, userIds o broadcast=true', 73 | error: 'INVALID_PARAMETERS', 74 | }; 75 | } catch (error) { 76 | this.logger.error('Error al enviar notificación:', error); 77 | return { 78 | success: false, 79 | message: 'Error al enviar notificación', 80 | error: error.message, 81 | }; 82 | } 83 | } 84 | 85 | async getNotifications(userId: string, offset: number = 0, limit: number = 20) { 86 | try { 87 | const notifications = await this.redisService.getNotifications(userId, offset, limit); 88 | const unreadCount = await this.redisService.getUnreadCount(userId); 89 | 90 | return { 91 | success: true, 92 | data: { 93 | notifications, 94 | unreadCount, 95 | pagination: { 96 | offset, 97 | limit, 98 | total: notifications.length, 99 | }, 100 | }, 101 | }; 102 | } catch (error) { 103 | this.logger.error('Error al obtener notificaciones:', error); 104 | return { 105 | success: false, 106 | message: 'Error al obtener notificaciones', 107 | error: error.message, 108 | }; 109 | } 110 | } 111 | 112 | async markAsRead(notificationId: string, userId: string) { 113 | try { 114 | const success = await this.redisService.markNotificationAsRead(userId, notificationId); 115 | 116 | if (success) { 117 | const unreadCount = await this.redisService.getUnreadCount(userId); 118 | 119 | // Notificar al usuario vía WebSocket sobre el cambio 120 | const socketId = await this.redisService.getConnectedUser(userId); 121 | if (socketId) { 122 | this.notificationsGateway.server.to(`user_${userId}`).emit('notification_read', { 123 | notificationId, 124 | unreadCount, 125 | }); 126 | } 127 | 128 | return { 129 | success: true, 130 | message: 'Notificación marcada como leída', 131 | unreadCount, 132 | }; 133 | } 134 | 135 | return { 136 | success: false, 137 | message: 'Notificación no encontrada', 138 | error: 'NOT_FOUND', 139 | }; 140 | } catch (error) { 141 | this.logger.error('Error al marcar notificación como leída:', error); 142 | return { 143 | success: false, 144 | message: 'Error al marcar notificación como leída', 145 | error: error.message, 146 | }; 147 | } 148 | } 149 | 150 | async getUnreadCount(userId: string) { 151 | try { 152 | const count = await this.redisService.getUnreadCount(userId); 153 | return { 154 | success: true, 155 | data: { 156 | userId, 157 | unreadCount: count, 158 | }, 159 | }; 160 | } catch (error) { 161 | this.logger.error('Error al obtener contador de no leídas:', error); 162 | return { 163 | success: false, 164 | message: 'Error al obtener contador de notificaciones no leídas', 165 | error: error.message, 166 | }; 167 | } 168 | } 169 | 170 | async getStats() { 171 | try { 172 | const connectedUsers = await this.redisService.getAllConnectedUsers(); 173 | const connectedCount = Object.keys(connectedUsers).length; 174 | 175 | return { 176 | success: true, 177 | data: { 178 | connectedUsers: connectedCount, 179 | timestamp: new Date().toISOString(), 180 | service: 'notification-service', 181 | status: 'active', 182 | }, 183 | }; 184 | } catch (error) { 185 | this.logger.error('Error al obtener estadísticas:', error); 186 | return { 187 | success: false, 188 | message: 'Error al obtener estadísticas', 189 | error: error.message, 190 | }; 191 | } 192 | } 193 | 194 | async broadcast(broadcastDto: Omit) { 195 | try { 196 | await this.notificationsGateway.broadcastNotification(broadcastDto); 197 | 198 | const connectedUsers = await this.redisService.getAllConnectedUsers(); 199 | const connectedCount = Object.keys(connectedUsers).length; 200 | 201 | return { 202 | success: true, 203 | message: 'Broadcast enviado exitosamente', 204 | data: { 205 | recipientCount: connectedCount, 206 | timestamp: new Date().toISOString(), 207 | }, 208 | }; 209 | } catch (error) { 210 | this.logger.error('Error en broadcast:', error); 211 | return { 212 | success: false, 213 | message: 'Error al enviar broadcast', 214 | error: error.message, 215 | }; 216 | } 217 | } 218 | 219 | async getConnectedUsers() { 220 | try { 221 | const connectedUsers = await this.redisService.getAllConnectedUsers(); 222 | 223 | return { 224 | success: true, 225 | data: { 226 | connectedUsers: Object.keys(connectedUsers), 227 | count: Object.keys(connectedUsers).length, 228 | timestamp: new Date().toISOString(), 229 | }, 230 | }; 231 | } catch (error) { 232 | this.logger.error('Error al obtener usuarios conectados:', error); 233 | return { 234 | success: false, 235 | message: 'Error al obtener usuarios conectados', 236 | error: error.message, 237 | }; 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /microservices/product-service/src/modules/product/product.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | NotFoundException, 4 | ConflictException, 5 | BadRequestException, 6 | } from '@nestjs/common'; 7 | import { InjectModel } from '@nestjs/mongoose'; 8 | import { Model, Types } from 'mongoose'; 9 | import { CreateProductDto } from './dto/create-product.dto'; 10 | import { UpdateProductDto } from './dto/update-product.dto'; 11 | import { AddReviewDto } from './dto/add-review.dto'; 12 | import { Product, ProductDocument } from './schemas/product.schema'; 13 | import { CategoryService } from '../category/category.service'; 14 | 15 | interface FindAllOptions { 16 | page: number; 17 | limit: number; 18 | search?: string; 19 | category?: string; 20 | minPrice?: number; 21 | maxPrice?: number; 22 | featured?: boolean; 23 | active?: boolean; 24 | sortBy: string; 25 | sortOrder: 'asc' | 'desc'; 26 | } 27 | 28 | @Injectable() 29 | export class ProductService { 30 | constructor( 31 | @InjectModel(Product.name) 32 | private productModel: Model, 33 | private categoryService: CategoryService, 34 | ) {} 35 | 36 | async create(createProductDto: CreateProductDto): Promise { 37 | // Verificar si el SKU ya existe 38 | const existingSku = await this.productModel.findOne({ 39 | sku: createProductDto.sku, 40 | }); 41 | 42 | if (existingSku) { 43 | throw new ConflictException('Ya existe un producto con ese SKU'); 44 | } 45 | 46 | // Verificar que la categoría existe 47 | await this.categoryService.findOne(createProductDto.categoryId); 48 | 49 | const product = new this.productModel({ 50 | ...createProductDto, 51 | categoryId: new Types.ObjectId(createProductDto.categoryId), 52 | }); 53 | 54 | return product.save(); 55 | } 56 | 57 | async findAll(options: FindAllOptions) { 58 | const { 59 | page, 60 | limit, 61 | search, 62 | category, 63 | minPrice, 64 | maxPrice, 65 | featured, 66 | active, 67 | sortBy, 68 | sortOrder, 69 | } = options; 70 | 71 | const skip = (page - 1) * limit; 72 | const filter: any = {}; 73 | 74 | // Filtro de búsqueda por texto 75 | if (search) { 76 | filter.$text = { $search: search }; 77 | } 78 | 79 | // Filtro por categoría 80 | if (category) { 81 | filter.categoryId = new Types.ObjectId(category); 82 | } 83 | 84 | // Filtro por rango de precios 85 | if (minPrice !== undefined || maxPrice !== undefined) { 86 | filter.price = {}; 87 | if (minPrice !== undefined) filter.price.$gte = minPrice; 88 | if (maxPrice !== undefined) filter.price.$lte = maxPrice; 89 | } 90 | 91 | // Filtro por productos destacados 92 | if (featured !== undefined) { 93 | filter.isFeatured = featured; 94 | } 95 | 96 | // Filtro por productos activos 97 | if (active !== undefined) { 98 | filter.isActive = active; 99 | } 100 | 101 | // Configurar ordenamiento 102 | const sortOptions: any = {}; 103 | sortOptions[sortBy] = sortOrder === 'asc' ? 1 : -1; 104 | 105 | const [products, total] = await Promise.all([ 106 | this.productModel 107 | .find(filter) 108 | .populate('categoryId', 'name slug') 109 | .skip(skip) 110 | .limit(limit) 111 | .sort(sortOptions) 112 | .exec(), 113 | this.productModel.countDocuments(filter), 114 | ]); 115 | 116 | return { 117 | data: products, 118 | total, 119 | page, 120 | limit, 121 | totalPages: Math.ceil(total / limit), 122 | }; 123 | } 124 | 125 | async findOne(id: string): Promise { 126 | if (!Types.ObjectId.isValid(id)) { 127 | throw new BadRequestException('ID de producto inválido'); 128 | } 129 | 130 | const product = await this.productModel 131 | .findById(id) 132 | .populate('categoryId', 'name slug') 133 | .exec(); 134 | 135 | if (!product) { 136 | throw new NotFoundException('Producto no encontrado'); 137 | } 138 | 139 | return product; 140 | } 141 | 142 | async findBySku(sku: string): Promise { 143 | const product = await this.productModel 144 | .findOne({ sku }) 145 | .populate('categoryId', 'name slug') 146 | .exec(); 147 | 148 | if (!product) { 149 | throw new NotFoundException('Producto no encontrado'); 150 | } 151 | 152 | return product; 153 | } 154 | 155 | async findByCategory(categoryId: string, page: number = 1, limit: number = 10) { 156 | if (!Types.ObjectId.isValid(categoryId)) { 157 | throw new BadRequestException('ID de categoría inválido'); 158 | } 159 | 160 | // Verificar que la categoría existe 161 | await this.categoryService.findOne(categoryId); 162 | 163 | const skip = (page - 1) * limit; 164 | const filter = { 165 | categoryId: new Types.ObjectId(categoryId), 166 | isActive: true, 167 | }; 168 | 169 | const [products, total] = await Promise.all([ 170 | this.productModel 171 | .find(filter) 172 | .populate('categoryId', 'name slug') 173 | .skip(skip) 174 | .limit(limit) 175 | .sort({ createdAt: -1 }) 176 | .exec(), 177 | this.productModel.countDocuments(filter), 178 | ]); 179 | 180 | return { 181 | data: products, 182 | total, 183 | page, 184 | limit, 185 | totalPages: Math.ceil(total / limit), 186 | }; 187 | } 188 | 189 | async update(id: string, updateProductDto: UpdateProductDto): Promise { 190 | const product = await this.findOne(id); 191 | 192 | // Si se está actualizando el SKU, verificar que no exista 193 | if (updateProductDto.sku && updateProductDto.sku !== product.sku) { 194 | const existingSku = await this.productModel.findOne({ 195 | sku: updateProductDto.sku, 196 | _id: { $ne: id }, 197 | }); 198 | 199 | if (existingSku) { 200 | throw new ConflictException('Ya existe un producto con ese SKU'); 201 | } 202 | } 203 | 204 | // Si se está actualizando la categoría, verificar que existe 205 | if (updateProductDto.categoryId) { 206 | await this.categoryService.findOne(updateProductDto.categoryId); 207 | updateProductDto.categoryId = new Types.ObjectId(updateProductDto.categoryId) as any; 208 | } 209 | 210 | const updatedProduct = await this.productModel 211 | .findByIdAndUpdate(id, updateProductDto, { new: true }) 212 | .populate('categoryId', 'name slug') 213 | .exec(); 214 | 215 | if (!updatedProduct) { 216 | throw new NotFoundException('Producto no encontrado'); 217 | } 218 | 219 | return updatedProduct; 220 | } 221 | 222 | async remove(id: string): Promise { 223 | const product = await this.findOne(id); 224 | await this.productModel.findByIdAndDelete(id).exec(); 225 | } 226 | 227 | async addReview(id: string, addReviewDto: AddReviewDto): Promise { 228 | const product = await this.productModel.findById(id).exec(); 229 | 230 | if (!product) { 231 | throw new NotFoundException('Producto no encontrado'); 232 | } 233 | 234 | // Verificar si el usuario ya ha dejado una reseña 235 | const existingReview = product.reviews.find( 236 | (review) => review.userId === addReviewDto.userId, 237 | ); 238 | 239 | if (existingReview) { 240 | throw new ConflictException('El usuario ya ha dejado una reseña para este producto'); 241 | } 242 | 243 | // Agregar la nueva reseña 244 | const newReview = { 245 | ...addReviewDto, 246 | createdAt: new Date(), 247 | }; 248 | 249 | product.reviews.push(newReview as any); 250 | 251 | // Recalcular la calificación promedio 252 | const totalRating = product.reviews.reduce((sum, review) => sum + review.rating, 0); 253 | product.averageRating = Math.round((totalRating / product.reviews.length) * 10) / 10; 254 | product.totalReviews = product.reviews.length; 255 | 256 | return await product.save(); 257 | } 258 | 259 | async updateStock(id: string, quantity: number): Promise { 260 | if (quantity < 0) { 261 | throw new BadRequestException('La cantidad no puede ser negativa'); 262 | } 263 | 264 | const product = await this.productModel 265 | .findByIdAndUpdate(id, { stock: quantity }, { new: true }) 266 | .populate('categoryId', 'name slug') 267 | .exec(); 268 | 269 | if (!product) { 270 | throw new NotFoundException('Producto no encontrado'); 271 | } 272 | 273 | return product; 274 | } 275 | 276 | async incrementViews(id: string): Promise { 277 | const product = await this.productModel 278 | .findByIdAndUpdate(id, { $inc: { views: 1 } }, { new: true }) 279 | .populate('categoryId', 'name slug') 280 | .exec(); 281 | 282 | if (!product) { 283 | throw new NotFoundException('Producto no encontrado'); 284 | } 285 | 286 | return product; 287 | } 288 | 289 | async getFeatured(limit: number = 10): Promise { 290 | return this.productModel 291 | .find({ isFeatured: true, isActive: true }) 292 | .populate('categoryId', 'name slug') 293 | .limit(limit) 294 | .sort({ createdAt: -1 }) 295 | .exec(); 296 | } 297 | 298 | async getPopular(limit: number = 10): Promise { 299 | return this.productModel 300 | .find({ isActive: true }) 301 | .populate('categoryId', 'name slug') 302 | .limit(limit) 303 | .sort({ totalSales: -1, views: -1 }) 304 | .exec(); 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sistema de Microservicios con NestJS 2 | 3 | ## 📋 Descripción 4 | 5 | Sistema completo de microservicios desarrollado con **NestJS** y **TypeScript**, que incluye: 6 | 7 | - 🌐 **API Gateway** - Punto de entrada principal 8 | - 👤 **User Service** - Gestión de usuarios con PostgreSQL 9 | - 📦 **Product Service** - Gestión de productos con MongoDB 10 | - 🔔 **Notification Service** - Notificaciones en tiempo real con WebSockets y Redis 11 | 12 | ## 🏗️ Arquitectura 13 | 14 | ``` 15 | ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ 16 | │ API Gateway │ │ User Service │ │ Product Service │ 17 | │ (Port 3000) │────│ (Port 3001) │ │ (Port 3002) │ 18 | │ │ │ PostgreSQL │ │ MongoDB │ 19 | └─────────────────┘ └─────────────────┘ └─────────────────┘ 20 | │ 21 | │ ┌─────────────────┐ 22 | └──────────────│Notification Svc │ 23 | │ (Port 3003) │ 24 | │ WebSockets+Redis│ 25 | └─────────────────┘ 26 | ``` 27 | 28 | ## 🚀 Inicio Rápido 29 | 30 | ### Prerrequisitos 31 | - Docker y Docker Compose 32 | - Node.js 20+ (para desarrollo local) 33 | - Git 34 | 35 | ### 1. Clonar el repositorio 36 | ```bash 37 | git clone https://github.com/jaimeirazabal1/microservicios-base-nestjs 38 | cd base_microservicios 39 | ``` 40 | 41 | ### 2. Configurar variables de entorno 42 | ```bash 43 | cp env.example .env 44 | # Editar .env según tus necesidades 45 | ``` 46 | 47 | ### 3. Iniciar todos los servicios 48 | ```bash 49 | # Opción 1: Script automatizado 50 | ./scripts/start-dev.sh 51 | 52 | # Opción 2: Docker Compose manual 53 | docker-compose up -d 54 | ``` 55 | 56 | ### 4. Verificar servicios 57 | - 🌐 API Gateway: http://localhost:3000 58 | - 👤 User Service: http://localhost:3001 59 | - 📦 Product Service: http://localhost:3002 60 | - 🔔 Notification Service: http://localhost:3003 61 | 62 | ## 📊 Servicios y Puertos 63 | 64 | | Servicio | Puerto | Base de Datos | Descripción | 65 | |----------|--------|---------------|-------------| 66 | | API Gateway | 3000 | - | Enrutamiento y autenticación | 67 | | User Service | 3001 | PostgreSQL:5432 | Gestión de usuarios | 68 | | Product Service | 3002 | MongoDB:27017 | Gestión de productos | 69 | | Notification Service | 3003 | Redis:6379 | Notificaciones tiempo real | 70 | 71 | ## 🔧 Desarrollo Local 72 | 73 | ### Instalar dependencias en todos los servicios 74 | ```bash 75 | ./scripts/build-all.sh 76 | ``` 77 | 78 | ### Ejecutar servicio individual 79 | ```bash 80 | cd microservices/[servicio] 81 | npm install 82 | npm run start:dev 83 | ``` 84 | 85 | ### Ver logs 86 | ```bash 87 | # Todos los servicios 88 | docker-compose logs -f 89 | 90 | # Servicio específico 91 | docker-compose logs -f user-service 92 | ``` 93 | 94 | ## 📡 API Endpoints 95 | 96 | ### Autenticación 97 | ```http 98 | POST /api/v1/auth/register # Registro 99 | POST /api/v1/auth/login # Login 100 | GET /api/v1/auth/profile # Perfil (JWT requerido) 101 | ``` 102 | 103 | ### Usuarios 104 | ```http 105 | GET /api/v1/users # Listar usuarios 106 | GET /api/v1/users/:id # Obtener usuario 107 | POST /api/v1/users # Crear usuario 108 | PUT /api/v1/users/:id # Actualizar usuario 109 | DELETE /api/v1/users/:id # Eliminar usuario 110 | ``` 111 | 112 | ### Productos 113 | ```http 114 | GET /api/v1/products # Listar productos 115 | GET /api/v1/products/:id # Obtener producto 116 | POST /api/v1/products # Crear producto 117 | PUT /api/v1/products/:id # Actualizar producto 118 | DELETE /api/v1/products/:id # Eliminar producto 119 | POST /api/v1/products/:id/reviews # Agregar reseña 120 | ``` 121 | 122 | ### Notificaciones 123 | ```http 124 | GET /api/v1/notifications # Obtener notificaciones 125 | POST /api/v1/notifications/send # Enviar notificación 126 | POST /api/v1/notifications/:id/read # Marcar como leída 127 | GET /api/v1/notifications/unread-count # Contador no leídas 128 | ``` 129 | 130 | ## 🔌 WebSockets (Notificaciones) 131 | 132 | ### Conexión 133 | ```javascript 134 | import io from 'socket.io-client'; 135 | 136 | const socket = io('http://localhost:3003/notifications', { 137 | query: { userId: 'user-id-here' } 138 | }); 139 | ``` 140 | 141 | ### Eventos disponibles 142 | ```javascript 143 | // Escuchar nueva notificación 144 | socket.on('new_notification', (notification) => { 145 | console.log('Nueva notificación:', notification); 146 | }); 147 | 148 | // Escuchar actualizaciones de contador 149 | socket.on('unread_count', (data) => { 150 | console.log('Notificaciones no leídas:', data.count); 151 | }); 152 | 153 | // Marcar como leída 154 | socket.emit('mark_as_read', { notificationId: 'notification-id' }); 155 | ``` 156 | 157 | ## 🗄️ Bases de Datos 158 | 159 | ### PostgreSQL (User Service) 160 | ```bash 161 | # Conectar 162 | docker exec -it microservices-postgres psql -U postgres -d userdb 163 | 164 | # Tablas principales: users 165 | ``` 166 | 167 | ### MongoDB (Product Service) 168 | ```bash 169 | # Conectar 170 | docker exec -it microservices-mongo mongosh -u mongo -p mongo123 171 | 172 | # Colecciones: products, categories 173 | ``` 174 | 175 | ### Redis (Notification Service) 176 | ```bash 177 | # Conectar 178 | docker exec -it microservices-redis redis-cli -a redis123 179 | 180 | # Keys: notifications:*, connected_users 181 | ``` 182 | 183 | ## 🛠️ Scripts Útiles 184 | 185 | ```bash 186 | # Iniciar desarrollo 187 | ./scripts/start-dev.sh 188 | 189 | # Detener servicios 190 | ./scripts/stop-dev.sh 191 | 192 | # Construir todos los servicios 193 | ./scripts/build-all.sh 194 | 195 | # Pruebas rápidas del sistema 196 | ./scripts/quick-test.sh 197 | 198 | # Pruebas completas automatizadas 199 | ./scripts/test-all.sh 200 | 201 | # Reconstruir imágenes 202 | docker-compose build --no-cache 203 | 204 | # Limpiar todo (cuidado: elimina datos) 205 | docker-compose down -v 206 | ``` 207 | 208 | ## 🧪 Pruebas y Testing 209 | 210 | ### Archivos de prueba disponibles: 211 | - **`test-endpoints.md`** - Guía completa con todos los comandos cURL 212 | - **`scripts/quick-test.sh`** - Verificación rápida de servicios 213 | - **`scripts/test-all.sh`** - Suite completa de pruebas automatizadas 214 | 215 | ### Ejecutar pruebas: 216 | ```bash 217 | # Verificación rápida (30 segundos) 218 | ./scripts/quick-test.sh 219 | 220 | # Suite completa de pruebas (2-3 minutos) 221 | ./scripts/test-all.sh 222 | 223 | # Ver guía completa de endpoints 224 | cat test-endpoints.md 225 | ``` 226 | 227 | ## 📦 Estructura del Proyecto 228 | 229 | ``` 230 | base_microservicios/ 231 | ├── microservices/ 232 | │ ├── api-gateway/ # Gateway principal 233 | │ ├── user-service/ # Servicio de usuarios 234 | │ ├── product-service/ # Servicio de productos 235 | │ └── notification-service/ # Servicio de notificaciones 236 | ├── shared/ # Código compartido 237 | │ ├── common/ # Interfaces y constantes 238 | │ └── config/ # Configuraciones 239 | ├── scripts/ # Scripts de automatización 240 | ├── docker-compose.yml # Orquestación de servicios 241 | ├── env.example # Variables de entorno ejemplo 242 | └── project_summary.json # Resumen del proyecto 243 | ``` 244 | 245 | ## 🔐 Autenticación 246 | 247 | El sistema usa **JWT (JSON Web Tokens)** para autenticación: 248 | 249 | 1. **Registro/Login** → Obtener token JWT 250 | 2. **Incluir token** en header: `Authorization: Bearer ` 251 | 3. **API Gateway** valida y reenvía peticiones 252 | 253 | ### Ejemplo de uso 254 | ```javascript 255 | // Login 256 | const response = await fetch('/api/v1/auth/login', { 257 | method: 'POST', 258 | headers: { 'Content-Type': 'application/json' }, 259 | body: JSON.stringify({ email: 'user@example.com', password: 'password' }) 260 | }); 261 | 262 | const { access_token } = await response.json(); 263 | 264 | // Usar token en peticiones 265 | const userResponse = await fetch('/api/v1/users/profile', { 266 | headers: { 'Authorization': `Bearer ${access_token}` } 267 | }); 268 | ``` 269 | 270 | ## 🚨 Solución de Problemas 271 | 272 | ### Servicios no inician 273 | ```bash 274 | # Verificar Docker 275 | docker --version 276 | docker-compose --version 277 | 278 | # Ver logs de error 279 | docker-compose logs [servicio] 280 | 281 | # Reiniciar servicios 282 | docker-compose restart 283 | ``` 284 | 285 | ### Problemas de conexión a BD 286 | ```bash 287 | # Verificar que las BD estén corriendo 288 | docker-compose ps 289 | 290 | # Esperar más tiempo para inicialización 291 | docker-compose up -d postgres mongodb redis 292 | sleep 30 293 | docker-compose up -d api-gateway user-service product-service notification-service 294 | ``` 295 | 296 | ### Puerto ocupado 297 | ```bash 298 | # Ver qué usa el puerto 299 | netstat -tulpn | grep :3000 300 | 301 | # Cambiar puerto en docker-compose.yml 302 | ``` 303 | 304 | ## 📈 Próximos Pasos 305 | 306 | - [ ] Tests unitarios y de integración 307 | - [ ] Documentación Swagger/OpenAPI 308 | - [ ] Circuit breakers y resilience 309 | - [ ] Monitoreo y métricas (Prometheus) 310 | - [ ] Logging centralizado (ELK Stack) 311 | - [ ] CI/CD pipeline 312 | - [ ] Rate limiting 313 | - [ ] Tracing distribuido 314 | 315 | ## 🤝 Contribución 316 | 317 | 1. Fork el proyecto 318 | 2. Crear rama feature (`git checkout -b feature/nueva-funcionalidad`) 319 | 3. Commit cambios (`git commit -am 'Agregar nueva funcionalidad'`) 320 | 4. Push a la rama (`git push origin feature/nueva-funcionalidad`) 321 | 5. Crear Pull Request 322 | 323 | ## 📄 Licencia 324 | 325 | Este proyecto está bajo la licencia UNLICENSED - ver el archivo LICENSE para detalles. 326 | 327 | --- 328 | 329 | **¡Listo para usar! 🎉** 330 | 331 | Para cualquier duda o problema, revisa los logs con `docker-compose logs -f` o consulta la documentación de cada servicio. -------------------------------------------------------------------------------- /scripts/test-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 🧪 Script de pruebas automatizadas para el sistema de microservicios 4 | # Uso: ./scripts/test-all.sh 5 | 6 | set -e # Salir si algún comando falla 7 | 8 | # Colores para output 9 | RED='\033[0;31m' 10 | GREEN='\033[0;32m' 11 | YELLOW='\033[1;33m' 12 | BLUE='\033[0;34m' 13 | NC='\033[0m' # No Color 14 | 15 | # Función para imprimir con colores 16 | print_status() { 17 | echo -e "${BLUE}[INFO]${NC} $1" 18 | } 19 | 20 | print_success() { 21 | echo -e "${GREEN}[SUCCESS]${NC} $1" 22 | } 23 | 24 | print_error() { 25 | echo -e "${RED}[ERROR]${NC} $1" 26 | } 27 | 28 | print_warning() { 29 | echo -e "${YELLOW}[WARNING]${NC} $1" 30 | } 31 | 32 | # Variables 33 | API_BASE="http://localhost:3000/api/v1" 34 | USER_SERVICE="http://localhost:3001" 35 | PRODUCT_SERVICE="http://localhost:3002" 36 | NOTIFICATION_SERVICE="http://localhost:3003" 37 | 38 | TEST_EMAIL="test-$(date +%s)@example.com" 39 | TEST_PASSWORD="123456" 40 | 41 | echo "🧪 Iniciando pruebas del sistema de microservicios..." 42 | echo "==================================================" 43 | 44 | # 1. Verificar que los servicios estén ejecutándose 45 | print_status "Verificando estado de los servicios..." 46 | 47 | if curl -s $API_BASE/health > /dev/null; then 48 | print_success "API Gateway (3000) - OK" 49 | else 50 | print_error "API Gateway (3000) - FAIL" 51 | exit 1 52 | fi 53 | 54 | if curl -s $USER_SERVICE/health > /dev/null; then 55 | print_success "User Service (3001) - OK" 56 | else 57 | print_error "User Service (3001) - FAIL" 58 | exit 1 59 | fi 60 | 61 | if curl -s $PRODUCT_SERVICE/health > /dev/null; then 62 | print_success "Product Service (3002) - OK" 63 | else 64 | print_error "Product Service (3002) - FAIL" 65 | exit 1 66 | fi 67 | 68 | if curl -s $NOTIFICATION_SERVICE/health > /dev/null; then 69 | print_success "Notification Service (3003) - OK" 70 | else 71 | print_error "Notification Service (3003) - FAIL" 72 | exit 1 73 | fi 74 | 75 | echo "" 76 | 77 | # 2. Prueba de registro de usuario 78 | print_status "Registrando usuario de prueba..." 79 | 80 | REGISTER_RESPONSE=$(curl -s -X POST $API_BASE/auth/register \ 81 | -H "Content-Type: application/json" \ 82 | -d '{ 83 | "email": "'$TEST_EMAIL'", 84 | "password": "'$TEST_PASSWORD'", 85 | "name": "Usuario Test", 86 | "lastName": "Automatizado" 87 | }') 88 | 89 | if echo "$REGISTER_RESPONSE" | jq -e '.access_token' > /dev/null; then 90 | TOKEN=$(echo $REGISTER_RESPONSE | jq -r '.access_token') 91 | USER_ID=$(echo $REGISTER_RESPONSE | jq -r '.user.id') 92 | print_success "Usuario registrado: $TEST_EMAIL" 93 | print_status "Token obtenido: ${TOKEN:0:30}..." 94 | else 95 | print_error "Fallo en registro de usuario" 96 | echo "$REGISTER_RESPONSE" | jq 97 | exit 1 98 | fi 99 | 100 | echo "" 101 | 102 | # 3. Prueba de login 103 | print_status "Probando login..." 104 | 105 | LOGIN_RESPONSE=$(curl -s -X POST $API_BASE/auth/login \ 106 | -H "Content-Type: application/json" \ 107 | -d '{ 108 | "email": "'$TEST_EMAIL'", 109 | "password": "'$TEST_PASSWORD'" 110 | }') 111 | 112 | if echo "$LOGIN_RESPONSE" | jq -e '.access_token' > /dev/null; then 113 | print_success "Login exitoso" 114 | else 115 | print_error "Fallo en login" 116 | echo "$LOGIN_RESPONSE" | jq 117 | exit 1 118 | fi 119 | 120 | echo "" 121 | 122 | # 4. Prueba de perfil de usuario 123 | print_status "Obteniendo perfil de usuario..." 124 | 125 | PROFILE_RESPONSE=$(curl -s -X GET $API_BASE/auth/profile \ 126 | -H "Authorization: Bearer $TOKEN") 127 | 128 | if echo "$PROFILE_RESPONSE" | jq -e '.email' > /dev/null; then 129 | print_success "Perfil obtenido correctamente" 130 | else 131 | print_error "Fallo al obtener perfil" 132 | echo "$PROFILE_RESPONSE" | jq 133 | fi 134 | 135 | echo "" 136 | 137 | # 5. Crear categoría de prueba 138 | print_status "Creando categoría de prueba..." 139 | 140 | CATEGORY_RESPONSE=$(curl -s -X POST $API_BASE/categories \ 141 | -H "Content-Type: application/json" \ 142 | -d '{ 143 | "name": "Pruebas Automatizadas", 144 | "description": "Categoría para pruebas del sistema", 145 | "slug": "pruebas-automatizadas" 146 | }') 147 | 148 | if echo "$CATEGORY_RESPONSE" | jq -e '._id' > /dev/null; then 149 | CATEGORY_ID=$(echo $CATEGORY_RESPONSE | jq -r '._id') 150 | print_success "Categoría creada: $CATEGORY_ID" 151 | else 152 | print_error "Fallo al crear categoría" 153 | echo "$CATEGORY_RESPONSE" | jq 154 | exit 1 155 | fi 156 | 157 | echo "" 158 | 159 | # 6. Crear producto de prueba 160 | print_status "Creando producto de prueba..." 161 | 162 | PRODUCT_RESPONSE=$(curl -s -X POST $API_BASE/products \ 163 | -H "Authorization: Bearer $TOKEN" \ 164 | -H "Content-Type: application/json" \ 165 | -d '{ 166 | "name": "Producto Test Automatizado", 167 | "description": "Producto creado por script de pruebas", 168 | "sku": "TEST-AUTO-'$(date +%s)'", 169 | "price": 99.99, 170 | "stock": 50, 171 | "categoryId": "'$CATEGORY_ID'", 172 | "tags": ["test", "automatizado"], 173 | "isFeatured": true 174 | }') 175 | 176 | if echo "$PRODUCT_RESPONSE" | jq -e '._id' > /dev/null; then 177 | PRODUCT_ID=$(echo $PRODUCT_RESPONSE | jq -r '._id') 178 | print_success "Producto creado: $PRODUCT_ID" 179 | else 180 | print_error "Fallo al crear producto" 181 | echo "$PRODUCT_RESPONSE" | jq 182 | exit 1 183 | fi 184 | 185 | echo "" 186 | 187 | # 7. Listar productos 188 | print_status "Listando productos..." 189 | 190 | PRODUCTS_LIST=$(curl -s -X GET "$API_BASE/products?limit=5") 191 | 192 | if echo "$PRODUCTS_LIST" | jq -e '.data' > /dev/null; then 193 | PRODUCT_COUNT=$(echo "$PRODUCTS_LIST" | jq '.data | length') 194 | print_success "Productos listados: $PRODUCT_COUNT encontrados" 195 | else 196 | print_error "Fallo al listar productos" 197 | echo "$PRODUCTS_LIST" | jq 198 | fi 199 | 200 | echo "" 201 | 202 | # 8. Agregar reseña al producto 203 | print_status "Agregando reseña al producto..." 204 | 205 | REVIEW_RESPONSE=$(curl -s -X POST $API_BASE/products/$PRODUCT_ID/reviews \ 206 | -H "Authorization: Bearer $TOKEN" \ 207 | -H "Content-Type: application/json" \ 208 | -d '{ 209 | "userId": "'$USER_ID'", 210 | "userName": "Usuario Test", 211 | "rating": 5, 212 | "comment": "Excelente producto de prueba!" 213 | }') 214 | 215 | if echo "$REVIEW_RESPONSE" | jq -e '.reviews' > /dev/null; then 216 | print_success "Reseña agregada correctamente" 217 | else 218 | print_error "Fallo al agregar reseña" 219 | echo "$REVIEW_RESPONSE" | jq 220 | fi 221 | 222 | echo "" 223 | 224 | # 9. Enviar notificación 225 | print_status "Enviando notificación de prueba..." 226 | 227 | NOTIFICATION_RESPONSE=$(curl -s -X POST $API_BASE/notifications/send \ 228 | -H "Content-Type: application/json" \ 229 | -d '{ 230 | "userId": "'$USER_ID'", 231 | "title": "Prueba Automatizada", 232 | "message": "Esta es una notificación de prueba del script automatizado", 233 | "type": "info", 234 | "data": { 235 | "testRun": true, 236 | "timestamp": "'$(date -Iseconds)'" 237 | } 238 | }') 239 | 240 | if echo "$NOTIFICATION_RESPONSE" | jq -e '.success' > /dev/null; then 241 | print_success "Notificación enviada correctamente" 242 | else 243 | print_error "Fallo al enviar notificación" 244 | echo "$NOTIFICATION_RESPONSE" | jq 245 | fi 246 | 247 | echo "" 248 | 249 | # 10. Obtener notificaciones del usuario 250 | print_status "Obteniendo notificaciones del usuario..." 251 | 252 | USER_NOTIFICATIONS=$(curl -s -X GET "$API_BASE/notifications?userId=$USER_ID&limit=5") 253 | 254 | if echo "$USER_NOTIFICATIONS" | jq -e '.data.notifications' > /dev/null; then 255 | NOTIFICATION_COUNT=$(echo "$USER_NOTIFICATIONS" | jq '.data.notifications | length') 256 | print_success "Notificaciones obtenidas: $NOTIFICATION_COUNT encontradas" 257 | else 258 | print_error "Fallo al obtener notificaciones" 259 | echo "$USER_NOTIFICATIONS" | jq 260 | fi 261 | 262 | echo "" 263 | 264 | # 11. Verificar bases de datos 265 | print_status "Verificando conexiones a bases de datos..." 266 | 267 | # PostgreSQL 268 | if docker exec microservices-postgres psql -U postgres -d userdb -c "SELECT COUNT(*) FROM users;" > /dev/null 2>&1; then 269 | USER_COUNT=$(docker exec microservices-postgres psql -U postgres -d userdb -t -c "SELECT COUNT(*) FROM users;" | tr -d ' ') 270 | print_success "PostgreSQL - $USER_COUNT usuarios en la base de datos" 271 | else 272 | print_warning "PostgreSQL - No se pudo verificar la conexión" 273 | fi 274 | 275 | # MongoDB 276 | if docker exec microservices-mongo mongosh -u mongo -p mongo123 productdb --quiet --eval "db.products.countDocuments()" > /dev/null 2>&1; then 277 | PRODUCT_COUNT_DB=$(docker exec microservices-mongo mongosh -u mongo -p mongo123 productdb --quiet --eval "print(db.products.countDocuments())") 278 | print_success "MongoDB - $PRODUCT_COUNT_DB productos en la base de datos" 279 | else 280 | print_warning "MongoDB - No se pudo verificar la conexión" 281 | fi 282 | 283 | # Redis 284 | if docker exec microservices-redis redis-cli -a redis123 ping > /dev/null 2>&1; then 285 | print_success "Redis - Conexión exitosa" 286 | else 287 | print_warning "Redis - No se pudo verificar la conexión" 288 | fi 289 | 290 | echo "" 291 | 292 | # Resumen final 293 | echo "🎉 RESUMEN DE PRUEBAS" 294 | echo "====================" 295 | print_success "✅ Servicios verificados: 4/4" 296 | print_success "✅ Usuario registrado: $TEST_EMAIL" 297 | print_success "✅ Categoría creada: $CATEGORY_ID" 298 | print_success "✅ Producto creado: $PRODUCT_ID" 299 | print_success "✅ Notificación enviada" 300 | print_success "✅ Bases de datos conectadas" 301 | 302 | echo "" 303 | print_status "🔗 URLs de prueba:" 304 | echo " - API Gateway: http://localhost:3000/api/v1" 305 | echo " - User Service: http://localhost:3001" 306 | echo " - Product Service: http://localhost:3002" 307 | echo " - Notification Service: http://localhost:3003" 308 | 309 | echo "" 310 | print_status "📊 Datos de prueba creados:" 311 | echo " - Usuario: $TEST_EMAIL (ID: $USER_ID)" 312 | echo " - Categoría: $CATEGORY_ID" 313 | echo " - Producto: $PRODUCT_ID" 314 | echo " - Token: ${TOKEN:0:30}..." 315 | 316 | echo "" 317 | print_success "🎊 ¡Todas las pruebas completadas exitosamente!" 318 | 319 | # Guardar información de prueba en archivo 320 | cat > test-results.json << EOF 321 | { 322 | "testRun": { 323 | "timestamp": "$(date -Iseconds)", 324 | "status": "success", 325 | "user": { 326 | "email": "$TEST_EMAIL", 327 | "id": "$USER_ID", 328 | "token": "$TOKEN" 329 | }, 330 | "category": { 331 | "id": "$CATEGORY_ID" 332 | }, 333 | "product": { 334 | "id": "$PRODUCT_ID" 335 | }, 336 | "services": { 337 | "apiGateway": "http://localhost:3000", 338 | "userService": "http://localhost:3001", 339 | "productService": "http://localhost:3002", 340 | "notificationService": "http://localhost:3003" 341 | } 342 | } 343 | } 344 | EOF 345 | 346 | print_status "📄 Resultados guardados en: test-results.json" 347 | -------------------------------------------------------------------------------- /test-endpoints.md: -------------------------------------------------------------------------------- 1 | # 🧪 Guía de Pruebas con cURL - Sistema de Microservicios 2 | # 3 | > ⭐ Si te resulta útil este proyecto, ¡ponle una estrellita al repositorio en GitHub! 4 | ## 📋 Índice 5 | - [Verificación de Servicios](#verificación-de-servicios) 6 | - [Autenticación](#autenticación) 7 | - [Gestión de Usuarios](#gestión-de-usuarios) 8 | - [Gestión de Productos](#gestión-de-productos) 9 | - [Categorías](#categorías) 10 | - [Notificaciones](#notificaciones) 11 | - [Pruebas de Estado y Salud](#pruebas-de-estado-y-salud) 12 | - [Pruebas de Base de Datos](#pruebas-de-base-de-datos) 13 | 14 | 15 | ## 🔍 Verificación de Servicios 16 | 17 | 18 | ### Verificar que todos los servicios estén ejecutándose 19 | ```bash 20 | # Ver estado de contenedores 21 | docker-compose ps 22 | 23 | # Ver logs de todos los servicios 24 | docker-compose logs --tail 20 25 | 26 | # Ver logs de un servicio específico 27 | docker-compose logs -f api-gateway 28 | docker-compose logs -f user-service 29 | docker-compose logs -f product-service 30 | docker-compose logs -f notification-service 31 | ``` 32 | 33 | ### Probar conectividad básica 34 | ```bash 35 | # API Gateway - Información general 36 | curl -s http://localhost:3000/api/v1 | jq 37 | 38 | # User Service - Información del servicio 39 | curl -s http://localhost:3001 | jq 40 | 41 | # Product Service - Información del servicio 42 | curl -s http://localhost:3002 | jq 43 | 44 | # Notification Service - Información del servicio 45 | curl -s http://localhost:3003 | jq 46 | ``` 47 | 48 | ### Endpoints de salud 49 | ```bash 50 | # API Gateway - Health check ----- 51 | curl -s http://localhost:3000/api/v1/health | jq 52 | 53 | # User Service - Health check 54 | curl -s http://localhost:3001/health | jq 55 | 56 | # Product Service - Health check 57 | curl -s http://localhost:3002/health | jq 58 | 59 | # Notification Service - Health check 60 | curl -s http://localhost:3003/health | jq 61 | ``` 62 | 63 | --- 64 | 65 | ## 🔐 Autenticación 66 | 67 | ### 1. Registrar un nuevo usuario 68 | ```bash 69 | # Registro básico 70 | curl -X POST http://localhost:3000/api/v1/auth/register \ 71 | -H "Content-Type: application/json" \ 72 | -d '{ 73 | "email": "admin@example.com", 74 | "password": "123456", 75 | "name": "Administrador", 76 | "lastName": "Sistema", 77 | "roles": ["admin"] 78 | }' | jq 79 | 80 | # Registro de usuario normal 81 | curl -X POST http://localhost:3000/api/v1/auth/register \ 82 | -H "Content-Type: application/json" \ 83 | -d '{ 84 | "email": "user@example.com", 85 | "password": "123456", 86 | "name": "Usuario", 87 | "lastName": "Test" 88 | }' | jq 89 | ``` 90 | 91 | ### 2. Iniciar sesión 92 | ```bash 93 | # Login y obtener token 94 | curl -X POST http://localhost:3000/api/v1/auth/login \ 95 | -H "Content-Type: application/json" \ 96 | -d '{ 97 | "email": "admin@example.com", 98 | "password": "123456" 99 | }' | jq 100 | 101 | # Guardar token en variable (reemplazar TOKEN con el token real) 102 | export TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." 103 | ``` 104 | 105 | ### 3. Obtener perfil del usuario autenticado 106 | ```bash 107 | curl -X GET http://localhost:3000/api/v1/auth/profile \ 108 | -H "Authorization: Bearer $TOKEN" | jq 109 | ``` 110 | 111 | ### 4. Renovar token 112 | ```bash 113 | curl -X POST http://localhost:3000/api/v1/auth/refresh \ 114 | -H "Authorization: Bearer $TOKEN" | jq 115 | ``` 116 | 117 | --- 118 | 119 | ## 👤 Gestión de Usuarios 120 | 121 | ### 1. Listar usuarios 122 | ```bash 123 | # Listar todos los usuarios 124 | curl -X GET http://localhost:3000/api/v1/users | jq 125 | 126 | # Listar con paginación 127 | curl -X GET "http://localhost:3000/api/v1/users?page=1&limit=5" | jq 128 | 129 | # Buscar usuarios 130 | curl -X GET "http://localhost:3000/api/v1/users?search=admin" | jq 131 | ``` 132 | 133 | ### 2. Obtener usuario específico 134 | ```bash 135 | # Reemplazar USER_ID con un ID real 136 | curl -X GET http://localhost:3000/api/v1/users/USER_ID | jq 137 | ``` 138 | 139 | ### 3. Actualizar usuario (requiere autenticación) 140 | ```bash 141 | curl -X PUT http://localhost:3000/api/v1/users/1ea861e6-3288-4a9a-927e-0e5bb54ec51e \ 142 | -H "Authorization: Bearer $TOKEN" \ 143 | -H "Content-Type: application/json" \ 144 | -d '{ 145 | "name": "Nombre Actualizado", 146 | "phone": "+1234567890" 147 | }' | jq 148 | ``` 149 | 150 | ### 4. Eliminar usuario (requiere autenticación) 151 | ```bash 152 | curl -X DELETE http://localhost:3000/api/v1/users/USER_ID \ 153 | -H "Authorization: Bearer $TOKEN" | jq 154 | ``` 155 | 156 | ### 5. Buscar usuario por email 157 | ```bash 158 | curl -X GET http://localhost:3000/api/v1/users/email/admin@example.com | jq 159 | ``` 160 | 161 | --- 162 | 163 | ## 📦 Gestión de Productos 164 | 165 | ### 1. Crear categoría primero 166 | ```bash 167 | curl -X POST http://localhost:3000/api/v1/categories \ 168 | -H "Content-Type: application/json" \ 169 | -d '{ 170 | "name": "Electrónicos", 171 | "description": "Productos electrónicos y gadgets", 172 | "slug": "electronicos" 173 | }' | jq 174 | 175 | # Guardar CATEGORY_ID para usar en productos 176 | export CATEGORY_ID="CATEGORY_ID_AQUI" 177 | ``` 178 | 179 | ### 2. Crear productos 180 | ```bash 181 | # Producto básico 182 | curl -X POST http://localhost:3000/api/v1/products \ 183 | -H "Authorization: Bearer $TOKEN" \ 184 | -H "Content-Type: application/json" \ 185 | -d '{ 186 | "name": "iPhone 15 Pro", 187 | "description": "El último iPhone con tecnología avanzada", 188 | "sku": "IPHONE15PRO", 189 | "price": 999.99, 190 | "stock": 50, 191 | "categoryId": "'$CATEGORY_ID'", 192 | "tags": ["smartphone", "apple", "premium"], 193 | "isFeatured": true 194 | }' | jq 195 | 196 | # Producto con más detalles 197 | curl -X POST http://localhost:3000/api/v1/products \ 198 | -H "Authorization: Bearer $TOKEN" \ 199 | -H "Content-Type: application/json" \ 200 | -d '{ 201 | "name": "MacBook Air M2", 202 | "description": "Laptop ultradelgada con chip M2", 203 | "sku": "MACBOOK-AIR-M2", 204 | "price": 1299.99, 205 | "comparePrice": 1399.99, 206 | "stock": 25, 207 | "categoryId": "'$CATEGORY_ID'", 208 | "tags": ["laptop", "apple", "m2"], 209 | "specifications": { 210 | "processor": "Apple M2", 211 | "ram": "8GB", 212 | "storage": "256GB SSD", 213 | "screen": "13.6 pulgadas" 214 | }, 215 | "isFeatured": true 216 | }' | jq 217 | ``` 218 | 219 | ### 3. Listar productos 220 | ```bash 221 | # Todos los productos 222 | curl -X GET http://localhost:3000/api/v1/products | jq 223 | 224 | # Con filtros y paginación 225 | curl -X GET "http://localhost:3000/api/v1/products?page=1&limit=10&featured=true" | jq 226 | 227 | # Buscar productos 228 | curl -X GET "http://localhost:3000/api/v1/products?search=iPhone" | jq 229 | 230 | # Filtrar por precio 231 | curl -X GET "http://localhost:3000/api/v1/products?minPrice=500&maxPrice=1500" | jq 232 | 233 | # Ordenar productos 234 | curl -X GET "http://localhost:3000/api/v1/products?sortBy=price&sortOrder=desc" | jq 235 | ``` 236 | 237 | ### 4. Obtener producto específico 238 | ```bash 239 | # Por ID 240 | curl -X GET http://localhost:3000/api/v1/products/PRODUCT_ID | jq 241 | 242 | # Por SKU 243 | curl -X GET http://localhost:3000/api/v1/products/sku/IPHONE15PRO | jq 244 | ``` 245 | 246 | ### 5. Productos destacados y populares 247 | ```bash 248 | # Productos destacados 249 | curl -X GET "http://localhost:3000/api/v1/products/featured/list?limit=5" | jq 250 | 251 | # Productos populares 252 | curl -X GET "http://localhost:3000/api/v1/products/popular/list?limit=5" | jq 253 | ``` 254 | 255 | ### 6. Actualizar producto 256 | ```bash 257 | curl -X PUT http://localhost:3000/api/v1/products/PRODUCT_ID \ 258 | -H "Authorization: Bearer $TOKEN" \ 259 | -H "Content-Type: application/json" \ 260 | -d '{ 261 | "price": 899.99, 262 | "stock": 75 263 | }' | jq 264 | ``` 265 | 266 | ### 7. Agregar reseña a producto 267 | ```bash 268 | curl -X POST http://localhost:3000/api/v1/products/PRODUCT_ID/reviews \ 269 | -H "Authorization: Bearer $TOKEN" \ 270 | -H "Content-Type: application/json" \ 271 | -d '{ 272 | "userId": "USER_ID", 273 | "userName": "Usuario Test", 274 | "rating": 5, 275 | "comment": "Excelente producto, muy recomendado!" 276 | }' | jq 277 | ``` 278 | 279 | ### 8. Actualizar stock 280 | ```bash 281 | curl -X PATCH http://localhost:3000/api/v1/products/PRODUCT_ID/stock \ 282 | -H "Authorization: Bearer $TOKEN" \ 283 | -H "Content-Type: application/json" \ 284 | -d '{"quantity": 100}' | jq 285 | ``` 286 | 287 | ### 9. Incrementar vistas 288 | ```bash 289 | curl -X PATCH http://localhost:3000/api/v1/products/PRODUCT_ID/view \ 290 | -H "Content-Type: application/json" | jq 291 | ``` 292 | 293 | --- 294 | 295 | ## 📂 Categorías 296 | 297 | ### 1. Listar categorías 298 | ```bash 299 | # Todas las categorías 300 | curl -X GET http://localhost:3000/api/v1/categories | jq 301 | 302 | # Con filtros 303 | curl -X GET "http://localhost:3000/api/v1/categories?active=true&search=electr" | jq 304 | ``` 305 | 306 | ### 2. Obtener categoría específica 307 | ```bash 308 | # Por ID 309 | curl -X GET http://localhost:3000/api/v1/categories/CATEGORY_ID | jq 310 | 311 | # Por slug 312 | curl -X GET http://localhost:3000/api/v1/categories/slug/electronicos | jq 313 | ``` 314 | 315 | ### 3. Crear más categorías 316 | ```bash 317 | # Categoría de ropa 318 | curl -X POST http://localhost:3000/api/v1/categories \ 319 | -H "Content-Type: application/json" \ 320 | -d '{ 321 | "name": "Ropa y Moda", 322 | "description": "Ropa, zapatos y accesorios de moda", 323 | "slug": "ropa-moda", 324 | "metadata": { 325 | "seasonalCategory": true, 326 | "targetAudience": "all" 327 | } 328 | }' | jq 329 | 330 | # Categoría de hogar 331 | curl -X POST http://localhost:3000/api/v1/categories \ 332 | -H "Content-Type: application/json" \ 333 | -d '{ 334 | "name": "Hogar y Jardín", 335 | "description": "Productos para el hogar y jardín", 336 | "slug": "hogar-jardin" 337 | }' | jq 338 | ``` 339 | 340 | ### 4. Productos por categoría 341 | ```bash 342 | curl -X GET "http://localhost:3000/api/v1/products/category/CATEGORY_ID?page=1&limit=5" | jq 343 | ``` 344 | 345 | --- 346 | 347 | ## 🔔 Notificaciones 348 | 349 | ### 1. Enviar notificación a usuario específico 350 | ```bash 351 | curl -X POST http://localhost:3000/api/v1/notifications/send \ 352 | -H "Content-Type: application/json" \ 353 | -d '{ 354 | "userId": "USER_ID", 355 | "title": "Bienvenido al sistema", 356 | "message": "Tu cuenta ha sido creada exitosamente", 357 | "type": "success", 358 | "data": { 359 | "action": "welcome", 360 | "timestamp": "'$(date -Iseconds)'" 361 | } 362 | }' | jq 363 | ``` 364 | 365 | ### 2. Enviar notificación a múltiples usuarios 366 | ```bash 367 | curl -X POST http://localhost:3000/api/v1/notifications/send \ 368 | -H "Content-Type: application/json" \ 369 | -d '{ 370 | "userIds": ["USER_ID_1", "USER_ID_2"], 371 | "title": "Oferta especial", 372 | "message": "50% de descuento en productos seleccionados", 373 | "type": "info", 374 | "data": { 375 | "discount": 50, 376 | "validUntil": "2024-12-31" 377 | } 378 | }' | jq 379 | ``` 380 | 381 | ### 3. Enviar notificación broadcast (a todos) 382 | ```bash 383 | curl -X POST http://localhost:3000/api/v1/notifications/send \ 384 | -H "Content-Type: application/json" \ 385 | -d '{ 386 | "broadcast": true, 387 | "title": "Mantenimiento programado", 388 | "message": "El sistema estará en mantenimiento el domingo de 2-4 AM", 389 | "type": "warning", 390 | "data": { 391 | "maintenanceDate": "2024-12-29", 392 | "duration": "2 hours" 393 | } 394 | }' | jq 395 | ``` 396 | 397 | ### 4. Obtener notificaciones de un usuario 398 | ```bash 399 | curl -X GET "http://localhost:3000/api/v1/notifications?userId=USER_ID&offset=0&limit=10" | jq 400 | ``` 401 | 402 | ### 5. Marcar notificación como leída 403 | ```bash 404 | curl -X POST http://localhost:3000/api/v1/notifications/NOTIFICATION_ID/read \ 405 | -H "Content-Type: application/json" \ 406 | -d '{"userId": "USER_ID"}' | jq 407 | ``` 408 | 409 | ### 6. Obtener contador de notificaciones no leídas 410 | ```bash 411 | curl -X GET "http://localhost:3000/api/v1/notifications/unread-count?userId=USER_ID" | jq 412 | ``` 413 | 414 | ### 7. Estadísticas del servicio de notificaciones 415 | ```bash 416 | curl -X GET http://localhost:3000/api/v1/notifications/stats | jq 417 | ``` 418 | 419 | ### 8. Usuarios conectados vía WebSocket 420 | ```bash 421 | curl -X GET http://localhost:3000/api/v1/notifications/connected-users | jq 422 | ``` 423 | 424 | --- 425 | 426 | ## 🏥 Pruebas de Estado y Salud 427 | 428 | ### Verificar conectividad de servicios 429 | ```bash 430 | # Ping a todos los servicios 431 | echo "=== API Gateway ===" && curl -s http://localhost:3000/api/v1/health | jq .status 432 | echo "=== User Service ===" && curl -s http://localhost:3001/health | jq .status 433 | echo "=== Product Service ===" && curl -s http://localhost:3002/health | jq .status 434 | echo "=== Notification Service ===" && curl -s http://localhost:3003/health | jq .status 435 | ``` 436 | 437 | ### Verificar bases de datos 438 | ```bash 439 | # PostgreSQL - Conexión directa 440 | docker exec -it microservices-postgres psql -U postgres -d userdb -c "SELECT version();" 441 | 442 | # MongoDB - Conexión directa 443 | docker exec -it microservices-mongo mongosh -u mongo -p mongo123 --eval "db.runCommand({ping: 1})" 444 | 445 | # Redis - Conexión directa 446 | docker exec -it microservices-redis redis-cli -a redis123 ping 447 | ``` 448 | 449 | --- 450 | 451 | ## 🗄️ Pruebas de Base de Datos 452 | 453 | ### PostgreSQL (User Service) 454 | ```bash 455 | # Ver usuarios en la base de datos 456 | docker exec -it microservices-postgres psql -U postgres -d userdb -c "SELECT id, email, name, \"isActive\", \"createdAt\" FROM users;" 457 | 458 | # Ver estructura de tabla users 459 | docker exec -it microservices-postgres psql -U postgres -d userdb -c "\\d users" 460 | 461 | # Contar usuarios 462 | docker exec -it microservices-postgres psql -U postgres -d userdb -c "SELECT COUNT(*) FROM users;" 463 | ``` 464 | 465 | ### MongoDB (Product Service) 466 | ```bash 467 | # Ver productos 468 | docker exec -it microservices-mongo mongosh -u mongo -p mongo123 productdb --eval "db.products.find().pretty()" 469 | 470 | # Ver categorías 471 | docker exec -it microservices-mongo mongosh -u mongo -p mongo123 productdb --eval "db.categories.find().pretty()" 472 | 473 | # Contar documentos 474 | docker exec -it microservices-mongo mongosh -u mongo -p mongo123 productdb --eval "db.products.countDocuments()" 475 | docker exec -it microservices-mongo mongosh -u mongo -p mongo123 productdb --eval "db.categories.countDocuments()" 476 | ``` 477 | 478 | ### Redis (Notification Service) 479 | ```bash 480 | # Ver todas las keys 481 | docker exec -it microservices-redis redis-cli -a redis123 KEYS "*" 482 | 483 | # Ver notificaciones de un usuario (reemplazar USER_ID) 484 | docker exec -it microservices-redis redis-cli -a redis123 LRANGE "notifications:USER_ID" 0 -1 485 | 486 | # Ver usuarios conectados 487 | docker exec -it microservices-redis redis-cli -a redis123 HGETALL "connected_users" 488 | 489 | # Estadísticas de Redis 490 | docker exec -it microservices-redis redis-cli -a redis123 INFO stats 491 | ``` 492 | 493 | --- 494 | 495 | ## 🚀 Scripts de Prueba Automatizados 496 | 497 | ### Script completo de pruebas 498 | ```bash 499 | #!/bin/bash 500 | # test-all.sh - Ejecutar todas las pruebas básicas 501 | 502 | echo "🧪 Iniciando pruebas del sistema de microservicios..." 503 | 504 | # 1. Verificar servicios 505 | echo "📊 Verificando servicios..." 506 | curl -s http://localhost:3000/api/v1/health > /dev/null && echo "✅ API Gateway OK" || echo "❌ API Gateway FAIL" 507 | curl -s http://localhost:3001/health > /dev/null && echo "✅ User Service OK" || echo "❌ User Service FAIL" 508 | curl -s http://localhost:3002/health > /dev/null && echo "✅ Product Service OK" || echo "❌ Product Service FAIL" 509 | curl -s http://localhost:3003/health > /dev/null && echo "✅ Notification Service OK" || echo "❌ Notification Service FAIL" 510 | 511 | # 2. Registrar usuario de prueba 512 | echo "👤 Registrando usuario de prueba..." 513 | REGISTER_RESPONSE=$(curl -s -X POST http://localhost:3000/api/v1/auth/register \ 514 | -H "Content-Type: application/json" \ 515 | -d '{ 516 | "email": "test@example.com", 517 | "password": "123456", 518 | "name": "Usuario Test" 519 | }') 520 | 521 | TOKEN=$(echo $REGISTER_RESPONSE | jq -r '.access_token') 522 | echo "🔑 Token obtenido: ${TOKEN:0:20}..." 523 | 524 | # 3. Crear categoría 525 | echo "📂 Creando categoría..." 526 | CATEGORY_RESPONSE=$(curl -s -X POST http://localhost:3000/api/v1/categories \ 527 | -H "Content-Type: application/json" \ 528 | -d '{ 529 | "name": "Pruebas", 530 | "description": "Categoría de prueba", 531 | "slug": "pruebas" 532 | }') 533 | 534 | CATEGORY_ID=$(echo $CATEGORY_RESPONSE | jq -r '._id') 535 | echo "📂 Categoría creada: $CATEGORY_ID" 536 | 537 | # 4. Crear producto 538 | echo "📦 Creando producto..." 539 | PRODUCT_RESPONSE=$(curl -s -X POST http://localhost:3000/api/v1/products \ 540 | -H "Authorization: Bearer $TOKEN" \ 541 | -H "Content-Type: application/json" \ 542 | -d '{ 543 | "name": "Producto Test", 544 | "description": "Producto de prueba", 545 | "sku": "TEST-001", 546 | "price": 99.99, 547 | "stock": 10, 548 | "categoryId": "'$CATEGORY_ID'" 549 | }') 550 | 551 | PRODUCT_ID=$(echo $PRODUCT_RESPONSE | jq -r '._id') 552 | echo "📦 Producto creado: $PRODUCT_ID" 553 | 554 | echo "🎉 Pruebas completadas exitosamente!" 555 | ``` 556 | 557 | --- 558 | 559 | ## 📝 Notas Importantes 560 | 561 | ### Variables de entorno útiles 562 | ```bash 563 | # Configurar variables para pruebas 564 | export API_BASE="http://localhost:3000/api/v1" 565 | export USER_SERVICE="http://localhost:3001" 566 | export PRODUCT_SERVICE="http://localhost:3002" 567 | export NOTIFICATION_SERVICE="http://localhost:3003" 568 | ``` 569 | 570 | ### Puertos de servicios 571 | - **API Gateway**: 3000 572 | - **User Service**: 3001 573 | - **Product Service**: 3002 574 | - **Notification Service**: 3003 575 | - **PostgreSQL**: 5432 576 | - **MongoDB**: 28017 (externo, 27017 interno) 577 | - **Redis**: 6379 578 | 579 | ### Credenciales por defecto 580 | - **PostgreSQL**: `postgres:postgres123` 581 | - **MongoDB**: `mongo:mongo123` 582 | - **Redis**: password `redis123` 583 | 584 | ### Herramientas recomendadas 585 | - **jq**: Para formatear respuestas JSON 586 | - **Postman**: Para pruebas más avanzadas 587 | - **curl**: Para pruebas rápidas desde terminal 588 | --------------------------------------------------------------------------------