diff --git a/server/apps/courses/package-lock.json b/server/apps/courses/package-lock.json index b3934549..c7c25563 100644 --- a/server/apps/courses/package-lock.json +++ b/server/apps/courses/package-lock.json @@ -12,16 +12,18 @@ "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/mapped-types": "*", + "@nestjs/microservices": "^10.4.8", "@nestjs/mongoose": "^10.1.0", "@nestjs/platform-express": "^10.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cloudinary": "^2.2.0", "dotenv": "^16.4.5", - "mongoose": "^8.8.1", + "mongoose": "^8.8.2", "multer": "^1.4.5-lts.1", "reflect-metadata": "^0.2.0", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "zod": "^3.23.8" }, "devDependencies": { "@nestjs/cli": "^10.0.0", @@ -1710,6 +1712,64 @@ } } }, + "node_modules/@nestjs/microservices": { + "version": "10.4.8", + "resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-10.4.8.tgz", + "integrity": "sha512-HAQXQ4hxg1+pva4GeR+on8UYJr8+wL+bs6s9pgUgaGq+Pyg3LTldpfgcmu9l2ZBVB52LxoIQNTZ8eI9/g6lK1A==", + "license": "MIT", + "dependencies": { + "iterare": "1.2.1", + "tslib": "2.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@grpc/grpc-js": "*", + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "amqp-connection-manager": "*", + "amqplib": "*", + "cache-manager": "*", + "ioredis": "*", + "kafkajs": "*", + "mqtt": "*", + "nats": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@grpc/grpc-js": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + }, + "amqp-connection-manager": { + "optional": true + }, + "amqplib": { + "optional": true + }, + "cache-manager": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "kafkajs": { + "optional": true + }, + "mqtt": { + "optional": true + }, + "nats": { + "optional": true + } + } + }, "node_modules/@nestjs/mongoose": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/@nestjs/mongoose/-/mongoose-10.1.0.tgz", @@ -6860,9 +6920,9 @@ } }, "node_modules/mongoose": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.8.1.tgz", - "integrity": "sha512-l7DgeY1szT98+EKU8GYnga5WnyatAu+kOQ2VlVX1Mxif6A0Umt0YkSiksCiyGxzx8SPhGe9a53ND1GD4yVDrPA==", + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.8.2.tgz", + "integrity": "sha512-jCTSqDANfRzk909v4YoZQi7jlGRB2MTvgG+spVBc/BA4tOs1oWJr//V6yYujqNq9UybpOtsSfBqxI0dSOEFJHQ==", "license": "MIT", "dependencies": { "bson": "^6.7.0", @@ -9400,6 +9460,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/server/apps/courses/package.json b/server/apps/courses/package.json index 409670ce..597a3606 100644 --- a/server/apps/courses/package.json +++ b/server/apps/courses/package.json @@ -23,16 +23,18 @@ "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/mapped-types": "*", + "@nestjs/microservices": "^10.4.8", "@nestjs/mongoose": "^10.1.0", "@nestjs/platform-express": "^10.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "cloudinary": "^2.2.0", "dotenv": "^16.4.5", - "mongoose": "^8.8.1", + "mongoose": "^8.8.2", + "multer": "^1.4.5-lts.1", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", - "cloudinary": "^2.2.0", - "multer": "^1.4.5-lts.1" + "zod": "^3.23.8" }, "devDependencies": { "@nestjs/cli": "^10.0.0", diff --git a/server/apps/courses/src/app.module.ts b/server/apps/courses/src/app.module.ts index 7e25f24e..28b514f3 100644 --- a/server/apps/courses/src/app.module.ts +++ b/server/apps/courses/src/app.module.ts @@ -1,7 +1,6 @@ import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { CoursesModule } from './courses/courses.module'; // Importamos el módulo de cursos -import { LeccionesModule } from './lecciones/lecciones.module'; import * as dotenv from 'dotenv'; @@ -10,8 +9,7 @@ dotenv.config(); @Module({ imports: [ MongooseModule.forRoot(process.env.MONGO_URI), - CoursesModule, - LeccionesModule + CoursesModule ], }) export class AppModule {} diff --git a/server/apps/courses/src/clases/dto/clases.dto.ts b/server/apps/courses/src/clases/dto/clases.dto.ts deleted file mode 100644 index 75f8b42b..00000000 --- a/server/apps/courses/src/clases/dto/clases.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ - -// import { -// IsEmail, -// IsEmpty, -// IsNotEmpty, -// IsNumber, -// IsString, -// MaxLength, -// MinLength, -// Validate, -// } from 'class-validator'; -//import { MatchPassword } from 'src/decorators/matchPassword.decorators'; -export class ClasesDto { - idCurso:string; - title: string; - description: string; - videoUrl:string[]; - multimedia:string[]; - resena:string; -} \ No newline at end of file diff --git a/server/apps/courses/src/courses/course.shema.ts b/server/apps/courses/src/courses/course.shema.ts index 40f6dcf5..13de1a87 100644 --- a/server/apps/courses/src/courses/course.shema.ts +++ b/server/apps/courses/src/courses/course.shema.ts @@ -1,27 +1,27 @@ -import { Schema, Document } from 'mongoose'; +// import { Schema, Document } from 'mongoose'; -export const CourseSchema = new Schema({ - name: { type: String, required: true }, - description: { type: String, required: true }, - createAt:{type: Date, required: true,default:Date.now()}, - updateAt:{type: Date, required: true,default:Date.now()}, - userId:{type: String, required:true}, - price:{type:Number}, - plataforma:{type:String}, - reputacion:{type:String}, - fotoCurso:{type:String}, - tags:{type:String} -}); +// export const CourseSchema = new Schema({ +// name: { type: String, required: true }, +// description: { type: String, required: true }, +// createAt:{type: Date, required: true,default:Date.now()}, +// updateAt:{type: Date, required: true,default:Date.now()}, +// userId:{type: String, required:true}, +// price:{type:Number}, +// plataforma:{type:String}, +// reputacion:{type:String}, +// fotoCurso:{type:String}, +// tags:{type:String} +// }); -export interface Course extends Document { - id: string; - name: string; - description: string; - userId:string; - price:number; - plataforma:string; - reputacion:string; - fotoCurso:string; - tags:string; -} +// export interface Course extends Document { +// id: string; +// name: string; +// description: string; +// userId:string; +// price:number; +// plataforma:string; +// reputacion:string; +// fotoCurso:string; +// tags:string; +// } diff --git a/server/apps/courses/src/courses/courses.controller.ts b/server/apps/courses/src/courses/courses.controller.ts index 4a33abf3..eb814129 100644 --- a/server/apps/courses/src/courses/courses.controller.ts +++ b/server/apps/courses/src/courses/courses.controller.ts @@ -1,25 +1,42 @@ -import { Controller, Post, Body,Get, Param } from '@nestjs/common'; +import { Controller } from '@nestjs/common'; import { CoursesService } from './courses.service'; -import { CreateCourseDto } from './dto/create-course.dto'; // Importa el DTO +import { MessagePattern, RpcException } from '@nestjs/microservices'; +import { CreateCourseSchema } from './dto/create-course.dto'; @Controller('courses') export class CoursesController { constructor(private readonly coursesService: CoursesService) {} + + @MessagePattern({ cmd: 'createCourse' }) + async createCourse(courseData: any) { + // Validar los datos con Zod + const validationResult = CreateCourseSchema.safeParse(courseData); + if (!validationResult.success) { + throw new RpcException({ + statusCode: 400, + message: 'Validation error', + errors: validationResult.error.errors, + }); + } -@Get('/') - async getCourse(){ - return await this.coursesService.getCourse(); + // Llamar al servicio para guardar el curso + return this.coursesService.createCourse(validationResult.data); } -@Get('/:id') -async getCourseId(@Param('id') id:string) { - return await this.coursesService.getCourseId(id); -} +// @Get('/') +// async getCourse(){ +// return await this.coursesService.getCourse(); +// } - @Post() - async create(@Body() createCourseDto: CreateCourseDto){ - return await this.coursesService.create(createCourseDto); - } +// @Get('/:id') +// async getCourseId(@Param('id') id:string) { +// return await this.coursesService.getCourseId(id); +// } + +// @Post() +// async create(@Body() createCourseDto: CreateCourseDto){ +// return await this.coursesService.create(createCourseDto); +// } // Otros métodos del controlador (por ejemplo, para obtener cursos, eliminar, etc.) } diff --git a/server/apps/courses/src/courses/courses.module.ts b/server/apps/courses/src/courses/courses.module.ts index 0b2b3132..4e71d583 100644 --- a/server/apps/courses/src/courses/courses.module.ts +++ b/server/apps/courses/src/courses/courses.module.ts @@ -1,12 +1,13 @@ import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; +import { Course, CourseSchema } from './schemas/course.schema'; import { CoursesService } from './courses.service'; import { CoursesController } from './courses.controller'; -import { CourseSchema } from './course.shema'; // Importa el esquema + @Module({ imports: [ - MongooseModule.forFeature([{ name: 'Course', schema: CourseSchema }]), // Registra el modelo en el módulo + MongooseModule.forFeature([{ name: Course.name, schema: CourseSchema }]), // Registra el modelo en el módulo ], providers: [CoursesService], controllers: [CoursesController], diff --git a/server/apps/courses/src/courses/courses.service.ts b/server/apps/courses/src/courses/courses.service.ts index 6cf20342..0297cbab 100644 --- a/server/apps/courses/src/courses/courses.service.ts +++ b/server/apps/courses/src/courses/courses.service.ts @@ -1,27 +1,37 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; -import { Course } from './course.shema'; -import { CreateCourseDto } from './dto/create-course.dto'; +import { Course } from './schemas/course.schema'; @Injectable() export class CoursesService { - constructor( - @InjectModel('Course') private readonly courseModel: Model, // Inyecta el modelo Course - ) {} + constructor(@InjectModel(Course.name) private courseModel: Model) {} -async getCourse(){ - return this.courseModel.find(); -} -async getCourseId(id){ - return await this.courseModel.findById(id); + async createCourse(data: any) { + const newCourse = new this.courseModel(data); + return newCourse.save(); + } } + //constructor(@InjectModel(Course.name) private courseModel: Model) { } - // Método para crear un curso - async create(createCourseDto: CreateCourseDto): Promise { - const course = new this.courseModel(createCourseDto); // Usa el DTO para crear el curso - return course.save(); // Guarda el curso en la base de datos - } + // async createCourse(data: { title: string; description?: string }) { + // const newCourse = new this.courseModel(data); + // return newCourse.save(); + // } + + // async getAllCourses() { + // return this.courseModel.find().exec(); + // } + + // async getCourse(){ + // return this.courseModel.find(); + // } + // async getCourseId(id){ + // return await this.courseModel.findById(id); + // } - // Otros métodos de servicio (por ejemplo, para listar cursos, eliminar, etc.) -} \ No newline at end of file + // // Método para crear un curso + // async create(createCourseDto: CreateCourseDto): Promise { + // const course = new this.courseModel(createCourseDto); // Usa el DTO para crear el curso + // return course.save(); // Guarda el curso en la base de datos + // } \ No newline at end of file diff --git a/server/apps/courses/src/courses/dto/create-course.dto.ts b/server/apps/courses/src/courses/dto/create-course.dto.ts index 498bf80c..acd146da 100644 --- a/server/apps/courses/src/courses/dto/create-course.dto.ts +++ b/server/apps/courses/src/courses/dto/create-course.dto.ts @@ -1,32 +1,48 @@ -import { IsString, IsNotEmpty, IsNumber, Min} from 'class-validator'; +import { z } from 'zod'; -export class CreateCourseDto { - @IsString() - @IsNotEmpty() - name: string; +// Paso 1: Información básica del curso +export const StepOneSchema = z.object({ + title: z.string().min(3, { message: 'Title must be at least 3 characters long' }), + contentType: z.enum(['free', 'premium']), + courseType: z.enum(['appsheet', 'powerapps']), + kind: z.enum(['course', 'lesson']), +}); - @IsString() - @IsNotEmpty() - description: string; - - @IsString() - @IsNotEmpty() - userId:string; - - @IsNumber() - @Min(0) - price:number; - - @IsString() - plataforma:string; +// Paso 2: Descripción detallada +export const StepTwoSchema = z.object({ + basicDescription: z.string().optional(), + prerequisites: z.array(z.string()).optional(), + detailedContent: z.string().optional(), + imageUrl: z.string().url().optional(), +}); - @IsString() - reputacion:string; +// Paso 3: Módulos y lecciones +export const StepThreeSchema = z.object({ + modules: z + .array( + z.object({ + moduleTitle: z.string().min(3, { message: 'Module title must be at least 3 characters long' }), + moduleDescription: z.string().optional(), + lessons: z + .array( + z.object({ + lessonTitle: z.string().min(3, { message: 'Lesson title must be at least 3 characters long' }), + lessonDescription: z.string().optional(), + materialUrl: z.string().url().optional(), + uploadedMaterial: z.string().optional(), + videoUrl: z.string().url().optional(), + }), + ) + .optional(), + }), + ) + .optional(), +}); - @IsString() - fotoCurso:string; +// Combinar los tres pasos en un esquema general +export const CreateCourseSchema = StepOneSchema.merge(StepTwoSchema).merge(StepThreeSchema); - @IsString() - tags:string; - -} +export type StepOneDto = z.infer; +export type StepTwoDto = z.infer; +export type StepThreeDto = z.infer; +export type CreateCourseDto = z.infer; diff --git a/server/apps/courses/src/courses/schemas/course.schema.ts b/server/apps/courses/src/courses/schemas/course.schema.ts new file mode 100644 index 00000000..2423b513 --- /dev/null +++ b/server/apps/courses/src/courses/schemas/course.schema.ts @@ -0,0 +1,64 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Document } from 'mongoose'; +import { Module } from './lesson.module.schema'; // Importamos el subesquema de módulos +import { MergeInfo, MergeInfoSchema } from './merge-info.schema'; // Importamos el subesquema de mergeInfo + +@Schema() +export class Course extends Document { + @Prop({ required: true }) + userId: string; // ID del usuario creador + + @Prop({ required: true }) + title: string; // Título del curso o lección + + @Prop({ + type: String, + enum: ['free', 'premium'], + default: 'premium', + required: true, + }) + contentType: string; // Tipo de contenido: gratuito o premium + + @Prop({ + type: String, + enum: ['appsheet', 'powerapps'], + required: true, + }) + courseType: string; // Tipo de curso: appsheet o powerapps + + @Prop({ + type: String, + enum: ['course', 'lesson'], + required: true, + }) + kind: string; // Indica si es un curso o lección + + @Prop() + basicDescription?: string; // Descripción básica del curso + + @Prop({ type: [String] }) + prerequisites?: string[]; // Requisitos previos del curso + + @Prop() + detailedContent?: string; // Descripción detallada del contenido + + @Prop() + imageUrl?: string; // URL de la imagen del curso + + @Prop({ type: [Module], default: [] }) + modules?: Module[]; // Lista de módulos del curso + + @Prop({ type: MergeInfoSchema }) + mergeInfo?: MergeInfo; // Información sobre fusiones con otras apps o cursos + + @Prop({ default: 'in-progress' }) + status: string; // Estado del curso + + @Prop({ default: Date.now }) + createdAt: Date; // Fecha de creación + + @Prop({ default: Date.now }) + updatedAt: Date; // Fecha de última actualización +} + +export const CourseSchema = SchemaFactory.createForClass(Course); diff --git a/server/apps/courses/src/courses/schemas/lesson.module.schema.ts b/server/apps/courses/src/courses/schemas/lesson.module.schema.ts new file mode 100644 index 00000000..34936595 --- /dev/null +++ b/server/apps/courses/src/courses/schemas/lesson.module.schema.ts @@ -0,0 +1,36 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Document } from 'mongoose'; + +@Schema() +export class Lesson extends Document { + @Prop({ required: true }) + lessonTitle: string; // Título de la lección + + @Prop() + lessonDescription?: string; // Descripción de la lección + + @Prop() + materialUrl?: string; // URL del material + + @Prop() + uploadedMaterial?: string; // Material subido + + @Prop() + videoUrl?: string; // URL del video +} + +export const LessonSchema = SchemaFactory.createForClass(Lesson); + +@Schema() +export class Module extends Document { + @Prop({ required: true }) + moduleTitle: string; // Título del módulo + + @Prop() + moduleDescription?: string; // Descripción del módulo + + @Prop({ type: [Lesson], default: [] }) + lessons?: Lesson[]; // Lecciones del módulo +} + +export const ModuleSchema = SchemaFactory.createForClass(Module); diff --git a/server/apps/courses/src/courses/schemas/merge-info.schema.ts b/server/apps/courses/src/courses/schemas/merge-info.schema.ts new file mode 100644 index 00000000..35f8c6e2 --- /dev/null +++ b/server/apps/courses/src/courses/schemas/merge-info.schema.ts @@ -0,0 +1,13 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Document } from 'mongoose'; + +@Schema() +export class MergeInfo extends Document { + @Prop({ type: Number, required: false }) + discountPercentage?: number; // Porcentaje de descuento + + @Prop({ type: [String], required: false }) + relatedCourses?: string[]; // IDs de cursos relacionados +} + +export const MergeInfoSchema = SchemaFactory.createForClass(MergeInfo); diff --git a/server/apps/courses/src/lecciones/lecciones.controller.ts b/server/apps/courses/src/lecciones/lecciones.controller.ts index 1b3dc22b..7f36010f 100644 --- a/server/apps/courses/src/lecciones/lecciones.controller.ts +++ b/server/apps/courses/src/lecciones/lecciones.controller.ts @@ -1,34 +1,33 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; +import { Controller} from '@nestjs/common'; import { LeccionesService } from './lecciones.service'; -import { CreateLeccioneDto } from './dto/create-leccione.dto'; -import { UpdateLeccioneDto } from './dto/update-leccione.dto'; + @Controller('lecciones') export class LeccionesController { constructor(private readonly leccionesService: LeccionesService) {} - @Post() - create(@Body() createLeccioneDto: CreateLeccioneDto) { - return this.leccionesService.create(createLeccioneDto); - } + // @Post() + // create(@Body() createLeccioneDto: CreateLeccioneDto) { + // return this.leccionesService.create(createLeccioneDto); + // } - @Get() - findAll() { - return this.leccionesService.findAll(); - } + // @Get() + // findAll() { + // return this.leccionesService.findAll(); + // } - @Get(':id') - findOne(@Param('id') id: string) { - return this.leccionesService.findOne(+id); - } + // @Get(':id') + // findOne(@Param('id') id: string) { + // return this.leccionesService.findOne(+id); + // } - @Patch(':id') - update(@Param('id') id: string, @Body() updateLeccioneDto: UpdateLeccioneDto) { - return this.leccionesService.update(+id, updateLeccioneDto); - } + // @Patch(':id') + // update(@Param('id') id: string, @Body() updateLeccioneDto: UpdateLeccioneDto) { + // return this.leccionesService.update(+id, updateLeccioneDto); + // } - @Delete(':id') - remove(@Param('id') id: string) { - return this.leccionesService.remove(+id); - } + // @Delete(':id') + // remove(@Param('id') id: string) { + // return this.leccionesService.remove(+id); + // } } diff --git a/server/apps/courses/src/lecciones/lecciones.service.ts b/server/apps/courses/src/lecciones/lecciones.service.ts index 378787d8..2c1deaa8 100644 --- a/server/apps/courses/src/lecciones/lecciones.service.ts +++ b/server/apps/courses/src/lecciones/lecciones.service.ts @@ -1,27 +1,26 @@ import { Injectable } from '@nestjs/common'; -import { CreateLeccioneDto } from './dto/create-leccione.dto'; -import { UpdateLeccioneDto } from './dto/update-leccione.dto'; + @Injectable() export class LeccionesService { - constructor() - create(createLeccioneDto: CreateLeccioneDto) { - return 'This action adds a new leccione'; - } + //constructor() + // create(createLeccioneDto: CreateLeccioneDto) { + // return 'This action adds a new leccione'; + // } - findAll() { - return `This action returns all lecciones`; - } + // findAll() { + // return `This action returns all lecciones`; + // } - findOne(id: number) { - return `This action returns a #${id} leccione`; - } + // findOne(id: number) { + // return `This action returns a #${id} leccione`; + // } - update(id: number, updateLeccioneDto: UpdateLeccioneDto) { - return `This action updates a #${id} leccione`; - } + // update(id: number, updateLeccioneDto: UpdateLeccioneDto) { + // return `This action updates a #${id} leccione`; + // } - remove(id: number) { - return `This action removes a #${id} leccione`; - } + // remove(id: number) { + // return `This action removes a #${id} leccione`; + // } } diff --git a/server/apps/courses/src/main.ts b/server/apps/courses/src/main.ts index 159e6a74..8989794c 100644 --- a/server/apps/courses/src/main.ts +++ b/server/apps/courses/src/main.ts @@ -1,17 +1,24 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; -import { ValidationPipe } from '@nestjs/common'; -//import { Logger } from '@nestjs/common'; +import * as dotenv from 'dotenv'; +dotenv.config(); +import { Transport, MicroserviceOptions } from '@nestjs/microservices'; async function bootstrap() { - console.log(process.env.PORT); - const app = await NestFactory.create(AppModule); - app.useGlobalPipes( - new ValidationPipe({ - whitelist: true, - forbidNonWhitelisted: true, - }) - ); - await app.listen(process.env.PORT ?? 3002); + + const app = await NestFactory.createMicroservice( + AppModule, + { + transport: Transport.TCP, + options: { + host: process.env.MICROSERVICE_HOST || '0.0.0.0', + port: parseInt(process.env.MICROSERVICE_PORT, 10) || 3002, + }, + }, + ); + + await app.listen(); + console.log('Microservice Courses is listening...'); } + bootstrap(); diff --git a/server/apps/courses/src/suscripto/dto/suscrito.dto.ts b/server/apps/courses/src/suscripto/dto/suscrito.dto.ts deleted file mode 100644 index 19a45f20..00000000 --- a/server/apps/courses/src/suscripto/dto/suscrito.dto.ts +++ /dev/null @@ -1,18 +0,0 @@ -// import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; -// import { -// IsEmail, -// IsEmpty, -// IsNotEmpty, -// IsNumber, -// IsString, -// MaxLength, -// MinLength, -// Validate, -// } from 'class-validator'; -// import { MatchPassword } from 'src/decorators/matchPassword.decorators'; -export class SuscritoDto { - id: string; - courseId: string; - userId: string; - progreso: string; - } \ No newline at end of file diff --git a/server/apps/courses/src/suscripto/entities/suscrito.entity.ts b/server/apps/courses/src/suscripto/entities/suscrito.entity.ts deleted file mode 100644 index ace854d1..00000000 --- a/server/apps/courses/src/suscripto/entities/suscrito.entity.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Entity, - Column, - PrimaryGeneratedColumn, - // ManyToOne, - // OneToOne -} from 'typeorm'; -// import { Users } from 'src/users/entities/users.entity'; -// import { Cursos } from 'src/cursos/entities/cursos.entity'; - - -@Entity({ - name: 'suscribe', -}) -export class Suscrito { - @PrimaryGeneratedColumn('uuid') - id: string; - @Column() - courseId: string; - @Column() - userId: string; - @Column() - progreso: string; - - - // En Curso.entity -// @ManyToOne(() => Suscrito, (suscrito) => suscrito.Cursos) -// suscriptores: Suscrito[]; - -// En Usuario.entity -// @ManyToOne(() => Suscrito, (suscrito) => suscrito.users) -// suscripciones: Suscrito[]; -} \ No newline at end of file diff --git a/server/apps/courses/src/suscripto/suscripto.controller.spec.ts b/server/apps/courses/src/suscripto/suscripto.controller.spec.ts deleted file mode 100644 index 206e2f45..00000000 --- a/server/apps/courses/src/suscripto/suscripto.controller.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { SuscriptoController } from './suscripto.controller'; - -describe('SuscriptoController', () => { - let controller: SuscriptoController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [SuscriptoController], - }).compile(); - - controller = module.get(SuscriptoController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/server/apps/courses/src/suscripto/suscripto.controller.ts b/server/apps/courses/src/suscripto/suscripto.controller.ts deleted file mode 100644 index f08ee70a..00000000 --- a/server/apps/courses/src/suscripto/suscripto.controller.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {Post, Controller, Get, Body } from '@nestjs/common'; -import { SuscriptoService } from './suscripto.service'; -import { SuscritoDto } from './dto/suscrito.dto'; - -@Controller('suscripto') -export class SuscriptoController { - constructor(private readonly suscriptoService: SuscriptoService) {} - - @Get() - async getSuscritos(){ - return await this.suscriptoService.getSuscritos(); - } - - @Post() - async postSuscripto(@Body() suscrito:SuscritoDto){ - const su = await this.suscriptoService.postSuscripto(suscrito); - return su; - } - - - -} diff --git a/server/apps/courses/src/suscripto/suscripto.module.ts b/server/apps/courses/src/suscripto/suscripto.module.ts deleted file mode 100644 index 766deb44..00000000 --- a/server/apps/courses/src/suscripto/suscripto.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { SuscriptoController } from './suscripto.controller'; -import { SuscriptoService } from './suscripto.service'; -import { Suscrito } from './entities/suscrito.entity'; - -@Module({ - imports: [ - TypeOrmModule.forFeature([Suscrito]) - ], - controllers: [SuscriptoController], - providers: [SuscriptoService] -}) -export class SuscriptoModule {} diff --git a/server/apps/courses/src/suscripto/suscripto.service.spec.ts b/server/apps/courses/src/suscripto/suscripto.service.spec.ts deleted file mode 100644 index 07423eff..00000000 --- a/server/apps/courses/src/suscripto/suscripto.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { SuscriptoService } from './suscripto.service'; - -describe('SuscriptoService', () => { - let service: SuscriptoService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [SuscriptoService], - }).compile(); - - service = module.get(SuscriptoService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/server/apps/courses/src/suscripto/suscripto.service.ts b/server/apps/courses/src/suscripto/suscripto.service.ts deleted file mode 100644 index b70555a4..00000000 --- a/server/apps/courses/src/suscripto/suscripto.service.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Repository } from 'typeorm'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Suscrito } from './entities/suscrito.entity'; - -@Injectable() -export class SuscriptoService { - constructor( - @InjectRepository(Suscrito) private suscritoRepository:Repository, - ){} - - async getSuscritos(){ - return await this.suscritoRepository.find(); - } - - async postSuscripto(suscri:Suscrito){ - return await this.suscritoRepository.save(suscri); - } - - -} diff --git a/server/apps/gateway/.env.example b/server/apps/gateway/.env.example index c0c68b1c..a709c3df 100644 --- a/server/apps/gateway/.env.example +++ b/server/apps/gateway/.env.example @@ -1 +1,11 @@ -PORT=3000 \ No newline at end of file +PORT=3000 +FRONTEND_URL=http://frontend/ +JWT_SECRET=secret_tokenauth +#users +USERS_MICROSERVICE_HOST=0.0.0.0 +USERS_SERVICE_PORT=3001 +MICROSERVICE_HOST=0.0.0.0 +MICROSERVICE_PORT=3001 +#courses +COURSES_SERVICE_HOST=0.0.0.0 +COURSES_SERVICE_PORT=3002 \ No newline at end of file diff --git a/server/apps/gateway/package-lock.json b/server/apps/gateway/package-lock.json index 304f749f..bb132fbf 100644 --- a/server/apps/gateway/package-lock.json +++ b/server/apps/gateway/package-lock.json @@ -17,6 +17,7 @@ "cookie-parser": "^1.4.7", "dotenv": "^16.4.5", "express": "^4.21.1", + "jsonwebtoken": "^9.0.2", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "zod": "^3.23.8" @@ -3297,6 +3298,12 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -4025,6 +4032,15 @@ "dev": true, "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -6475,6 +6491,49 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -6565,6 +6624,42 @@ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -6579,6 +6674,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -7939,7 +8040,6 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" diff --git a/server/apps/gateway/package.json b/server/apps/gateway/package.json index 0fcfd8ed..1c00e27d 100644 --- a/server/apps/gateway/package.json +++ b/server/apps/gateway/package.json @@ -28,6 +28,7 @@ "cookie-parser": "^1.4.7", "dotenv": "^16.4.5", "express": "^4.21.1", + "jsonwebtoken": "^9.0.2", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "zod": "^3.23.8" diff --git a/server/apps/gateway/src/app.module.ts b/server/apps/gateway/src/app.module.ts index a09b2e97..bf0d70f6 100644 --- a/server/apps/gateway/src/app.module.ts +++ b/server/apps/gateway/src/app.module.ts @@ -1,8 +1,9 @@ import { Module } from '@nestjs/common'; import { AuthModule } from './auth/auth.module'; import { UploadModule } from './upload/upload.module'; +import { CoursesModule } from './courses/courses.module'; import { PubSubModule } from './pubsub/pubsub.module'; @Module({ - imports: [AuthModule, UploadModule, PubSubModule], + imports: [AuthModule, UploadModule, CoursesModule,PubSubModule], }) export class AppModule {} diff --git a/server/apps/gateway/src/auth/auth.controllers.ts b/server/apps/gateway/src/auth/auth.controllers.ts index 24090cd1..147cfbb3 100644 --- a/server/apps/gateway/src/auth/auth.controllers.ts +++ b/server/apps/gateway/src/auth/auth.controllers.ts @@ -7,6 +7,8 @@ import { BadRequestException, Patch, Response, + Get, + UseGuards, } from '@nestjs/common'; import { ClientProxy } from '@nestjs/microservices'; import { lastValueFrom } from 'rxjs'; @@ -15,6 +17,8 @@ import { UpdateSchema } from './dto/updateSchema.dto'; import { LoginDto } from './dto/loginSchema.dto'; import { CookieService } from 'src/common/services/cookie.service'; import { Response as ExpressResponse } from 'express'; +import { Roles } from 'src/decorators/roles.decorator'; +import { RolesGuard } from 'src/guards/roles.guard'; @Controller('auth') export class AuthController { @@ -38,10 +42,33 @@ export class AuthController { } @Post('verifyEmail') - async verifyEmail(@Body('token') token: string): Promise { - return lastValueFrom( - this.authClient.send({ cmd: 'verifyEmail' }, { token }), - ); + async verifyEmail( + @Body('token') token: string, + @Response() res: ExpressResponse, + ) { + try { + // Enviar la solicitud al microservicio para verificar el token de correo + const { token: newToken } = await lastValueFrom( + this.authClient.send({ cmd: 'verifyEmail' }, { token }), + ); + + // Usar el servicio CookieService para gestionar la cookie con el nuevo token + this.cookieService.set(res, 'auth_token', newToken, { + maxAge: 60 * 60 * 1000, // 1 hora + httpOnly: true, + secure: false, + sameSite: 'strict', + }); + + // Regresar una respuesta al cliente + return res.status(200).json({ + message: '¡Correo electrónico verificado y sesión iniciada!', + }); + } catch (error) { + throw new BadRequestException( + error.message || 'Error al verificar el correo', + ); + } } @Post('google') async google(@Body() token: string): Promise { @@ -86,4 +113,29 @@ export class AuthController { throw error; } } + // profile user + @Get('profile') + @Roles('admin', 'moderator', 'user') // Roles permitidos + @UseGuards(RolesGuard) // Verifica el rol del usuario + async getProfile(@Request() req: any, @Response() res: ExpressResponse) { + const userId = req.user.id; // Recuperamos el userId desde el token (verificado por el middleware) + + if (!userId) { + throw new BadRequestException('No se encontró el ID del usuario'); + } + + try { + // Enviar el userId al microservicio para obtener la información completa del perfil + const profile = await lastValueFrom( + this.authClient.send({ cmd: 'getProfile' }, { userId }), // Enviar userId al microservicio + ); + + // Regresar la información completa del perfil al cliente + return res.status(200).json(profile); + } catch (error) { + throw new BadRequestException( + error.message || 'Error al obtener el perfil', + ); + } + } } diff --git a/server/apps/gateway/src/courses/courses.controller.ts b/server/apps/gateway/src/courses/courses.controller.ts new file mode 100644 index 00000000..3bb29a5b --- /dev/null +++ b/server/apps/gateway/src/courses/courses.controller.ts @@ -0,0 +1,54 @@ +import { + Controller, + Post, + Body, + Request, + BadRequestException, + Inject, + UseGuards, +} from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { lastValueFrom } from 'rxjs'; +import { CreateCourseSchema, CreateCourseDto } from './dto/create-course.dto'; +import { CookieService } from 'src/common/services/cookie.service'; +import { Roles } from 'src/decorators/roles.decorator'; +import { RolesGuard } from 'src/guards/roles.guard'; + +@Controller('courses') +export class CoursesController { + constructor( + @Inject('COURSES_SERVICE') private readonly coursesClient: ClientProxy, + private readonly cookieService: CookieService, + ) {} + + @Post('create') + @Roles('admin', 'user') // para prueba se dejo en user luego se agrega el permiso correspondiente + @UseGuards(RolesGuard) + async createCourse(@Body() courseData: any, @Request() req: any) { + const userId = req.user?.Id; + + if (!userId) { + throw new BadRequestException('No se encontró el ID del usuario'); + } + try { + // Validar los datos con Zod + const validationResult = CreateCourseSchema.safeParse(courseData); + if (!validationResult.success) { + throw new BadRequestException(validationResult.error.errors); + } + + // Enviar los datos al microservicio con el userId + const createCourseDto: CreateCourseDto = { + ...validationResult.data, + userId, + }; + + const result = await lastValueFrom( + this.coursesClient.send({ cmd: 'createCourse' }, createCourseDto), + ); + return result; + } catch (error) { + throw new BadRequestException(error.message || 'Error al crear el curso'); + } + } +} diff --git a/server/apps/gateway/src/courses/courses.module.ts b/server/apps/gateway/src/courses/courses.module.ts new file mode 100644 index 00000000..04920ef5 --- /dev/null +++ b/server/apps/gateway/src/courses/courses.module.ts @@ -0,0 +1,24 @@ +import { Module } from '@nestjs/common'; +import { CoursesController } from './courses.controller'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { CookieService } from '../common/services/cookie.service'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +@Module({ + imports: [ + ClientsModule.register([ + { + name: 'COURSES_SERVICE', + transport: Transport.TCP, + options: { + host: process.env.COURSES_SERVICE_HOST, + port: parseInt(process.env.COURSES_SERVICE_PORT, 10), + }, + }, + ]), + ], + controllers: [CoursesController], + providers: [CookieService], +}) +export class CoursesModule {} diff --git a/server/apps/gateway/src/courses/dto/create-course.dto.ts b/server/apps/gateway/src/courses/dto/create-course.dto.ts new file mode 100644 index 00000000..c5be0e44 --- /dev/null +++ b/server/apps/gateway/src/courses/dto/create-course.dto.ts @@ -0,0 +1,58 @@ +import { z } from 'zod'; + +// Paso 1: Información básica del curso +export const StepOneSchema = z.object({ + title: z + .string() + .min(3, { message: 'Title must be at least 3 characters long' }), + contentType: z.enum(['free', 'premium']), + courseType: z.enum(['appsheet', 'powerapps']), + kind: z.enum(['course', 'lesson']), +}); + +// Paso 2: Descripción detallada +export const StepTwoSchema = z.object({ + basicDescription: z.string().optional(), + prerequisites: z.array(z.string()).optional(), + detailedContent: z.string().optional(), + imageUrl: z.string().url().optional(), +}); + +// Paso 3: Módulos y lecciones +export const StepThreeSchema = z.object({ + modules: z + .array( + z.object({ + moduleTitle: z.string().min(3, { + message: 'Module title must be at least 3 characters long', + }), + moduleDescription: z.string().optional(), + lessons: z + .array( + z.object({ + lessonTitle: z.string().min(3, { + message: 'Lesson title must be at least 3 characters long', + }), + lessonDescription: z.string().optional(), + materialUrl: z.string().url().optional(), + uploadedMaterial: z.string().optional(), + videoUrl: z.string().url().optional(), + }), + ) + .optional(), + }), + ) + .optional(), +}); + +// Combinar los tres pasos en un esquema general +export const CreateCourseSchema = StepOneSchema.merge(StepTwoSchema) + .merge(StepThreeSchema) + .extend({ + userId: z.string(), + }); + +export type StepOneDto = z.infer; +export type StepTwoDto = z.infer; +export type StepThreeDto = z.infer; +export type CreateCourseDto = z.infer; diff --git a/server/apps/gateway/src/decorators/roles.decorator.ts b/server/apps/gateway/src/decorators/roles.decorator.ts new file mode 100644 index 00000000..dea3d664 --- /dev/null +++ b/server/apps/gateway/src/decorators/roles.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +// Crear un decorador para asignar roles a las rutas +export const Roles = (...roles: string[]) => SetMetadata('roles', roles); diff --git a/server/apps/gateway/src/gateway.module.ts b/server/apps/gateway/src/gateway.module.ts index f9789cea..e81f268c 100644 --- a/server/apps/gateway/src/gateway.module.ts +++ b/server/apps/gateway/src/gateway.module.ts @@ -20,22 +20,22 @@ dotenv.config(); name: 'COURSES_SERVICE', transport: Transport.TCP, options: { - host: process.env.COURSES_SERVICE_HOST || 'localhost', + host: process.env.COURSES_SERVICE_HOST || '0.0.0.0', port: parseInt(process.env.COURSES_SERVICE_PORT, 10) || 3002, }, }, { - name: 'UPLOAD_SERVICE', - transport: Transport.GRPC, + name: 'UPLOAD_SERVICE', + transport: Transport.GRPC, options: { - package: 'googlecloudstorage', - protoPath: 'src/proto/upload.proto', - url: process.env.GRPC_SERVER_URL || 'localhost:50051', + package: 'googlecloudstorage', + protoPath: 'src/proto/upload.proto', + url: process.env.GRPC_SERVER_URL || 'localhost:50051', }, }, ]), ], - controllers: [AuthController,UploadController], + controllers: [AuthController, UploadController], providers: [], }) export class GatewayModule {} diff --git a/server/apps/gateway/src/guards/roles.guard.ts b/server/apps/gateway/src/guards/roles.guard.ts new file mode 100644 index 00000000..ade26387 --- /dev/null +++ b/server/apps/gateway/src/guards/roles.guard.ts @@ -0,0 +1,41 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + ForbiddenException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { Request } from 'express'; + +@Injectable() +export class RolesGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const requiredRoles = this.reflector.get( + 'roles', + context.getHandler(), + ); // Obtener roles necesarios de la ruta + + if (!requiredRoles) { + return true; // Si no se especifican roles, permitir acceso + } + + const request = context.switchToHttp().getRequest(); + const user = request['user']; // Obtener los datos del usuario del middleware + + if (!user) { + throw new ForbiddenException( + 'Acceso denegado, no se encontraron datos del usuario', + ); + } + + // Verificar si el rol del usuario está permitido + const hasRole = requiredRoles.includes(user.role); + if (!hasRole) { + throw new ForbiddenException('Acceso denegado, rol insuficiente'); + } + + return true; // Permitir acceso si el rol es adecuado + } +} diff --git a/server/apps/gateway/src/main.ts b/server/apps/gateway/src/main.ts index 44a791bc..6b37272e 100644 --- a/server/apps/gateway/src/main.ts +++ b/server/apps/gateway/src/main.ts @@ -3,17 +3,17 @@ import { AppModule } from './app.module'; import * as dotenv from 'dotenv'; dotenv.config(); import * as cookieParser from 'cookie-parser'; +import { AuthMiddleware } from './middleware/auth.middleware'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.use(cookieParser()); + app.use(new AuthMiddleware().use); app.enableCors({ - origin: process.env.FRONTEND_URL || 'http://localhost:3000', // Configuración de CORS + origin: process.env.FRONTEND_URL || 'http://frontend@example', // Configuración de CORS credentials: true, // Permitir cookies }); await app.listen(process.env.PORT || 3000); - console.log( - `Gateway is running on: http://localhost:${process.env.PORT || 3000}`, - ); + console.log(`Gateway is running on: ${process.env.PORT || 3000}`); } bootstrap(); diff --git a/server/apps/gateway/src/middleware/auth.middleware.ts b/server/apps/gateway/src/middleware/auth.middleware.ts new file mode 100644 index 00000000..9f201d44 --- /dev/null +++ b/server/apps/gateway/src/middleware/auth.middleware.ts @@ -0,0 +1,47 @@ +import { + Injectable, + NestMiddleware, + UnauthorizedException, +} from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; +import * as jwt from 'jsonwebtoken'; +import { publicRoutes } from './routersPublics'; // Importar las rutas públicas +import * as dotenv from 'dotenv'; +dotenv.config(); + +@Injectable() +export class AuthMiddleware implements NestMiddleware { + // Usamos una función de flecha para mantener el contexto de `this` + use = (req: Request, res: Response, next: NextFunction) => { + // Si la ruta actual es pública, omitimos la validación + if (this.isPublicRoute(req.originalUrl)) { + return next(); // Permitir acceso sin token + } + + // Obtener el token de las cookies + const token = req.cookies['auth_token']; + + if (!token) { + throw new UnauthorizedException('Token no proporcionado'); + } + + try { + // Decodificar el token JWT + const payload = jwt.verify(token, process.env.JWT_SECRET) as { + userId: string; + role: string; + }; + req['user'] = { id: payload.userId, role: payload.role }; // Adjuntar el usuario a la solicitud + + next(); // Continuar con la siguiente función + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + throw new UnauthorizedException('Token inválido o expirado'); + } + }; + + // Verificar si la ruta es pública + private isPublicRoute(url: string): boolean { + return publicRoutes.some((route) => url.startsWith(route)); // Comparar la URL con las rutas públicas + } +} diff --git a/server/apps/gateway/src/middleware/routersPublics.ts b/server/apps/gateway/src/middleware/routersPublics.ts new file mode 100644 index 00000000..3683697c --- /dev/null +++ b/server/apps/gateway/src/middleware/routersPublics.ts @@ -0,0 +1,5 @@ +export const publicRoutes: string[] = [ + '/auth/register', + '/auth/login', + '/auth/verifyEmail', +]; diff --git a/server/apps/users/.env.example b/server/apps/users/.env.example index bdaf352e..9847d3b3 100644 --- a/server/apps/users/.env.example +++ b/server/apps/users/.env.example @@ -1,6 +1,9 @@ +NODE_ENV=development +#NODE_ENV=production +JWT_SECRET=secret_tokenauth MICROSERVICE_HOST=0.0.0.0 MICROSERVICE_PORT=3001 -# smt +#smt SMTP_USER= SMTP_PASS= SMTP_HOST= @@ -8,13 +11,9 @@ SMTP_PORT= SMTP_SECURE= #POSTGRES_URL="postgresql://user_postgres:password_postgres@localhost:5432/testdb_postgres" -POSTGRES_URL= #Google GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= +GOOGLE_CALLBACK_URL= -#Secret -JWT_SECRET= - -#frontend -FRONTEND=http//:localhost:3000 \ No newline at end of file +POSTGRES_URL=postgres://neondb_owner:Goa5ZjUmlub1@ep-wandering-unit-a4k4nair-pooler.us-east-1.aws.neon.tech/neondb?sslmode=require diff --git a/server/apps/users/src/auth/auth.controller.ts b/server/apps/users/src/auth/auth.controller.ts index 282c8da0..ff7529a7 100644 --- a/server/apps/users/src/auth/auth.controller.ts +++ b/server/apps/users/src/auth/auth.controller.ts @@ -140,4 +140,36 @@ export class AuthController { await this.authService.resetPassword(email, token, newPassword); return { message: 'Contraseña restablecida exitosamente.' }; } + + // profile user + @MessagePattern({ cmd: 'getProfile' }) + async getProfile(data: { userId: string }) { + const { userId } = data; + + try { + // Buscar al usuario por su userId + const user = await this.authService.findUserById(userId); + if (!user) { + throw new RpcException({ + message: 'Usuario no encontrado', + statusCode: 404, + }); + } + + // Devolver los datos completos del perfil + return { + id: user.id, + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + role: user.role, + createdAt: user.createdAt, + }; + } catch (error) { + throw new RpcException({ + statusCode: 500, + message: error.message || 'Error al obtener el perfil', + }); + } + } } diff --git a/server/apps/users/src/auth/auth.service.ts b/server/apps/users/src/auth/auth.service.ts index 31f79941..6b9d3fcc 100644 --- a/server/apps/users/src/auth/auth.service.ts +++ b/server/apps/users/src/auth/auth.service.ts @@ -68,7 +68,7 @@ export class AuthService { // Generar un nuevo token de inicio de sesión const loginToken = jwt.sign( - { userId: user.id, email: user.email }, + { userId: user.id, email: user.email, role: user.role }, process.env.JWT_SECRET, { expiresIn: '1h' }, ); @@ -349,4 +349,18 @@ export class AuthService { return { message: 'Password reset successfully' }; } + // Verificar el token JWT + verifyJwt(token: string): any { + try { + return jwt.verify(token, process.env.JWT_SECRET); // Verifica el token con la clave secreta + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + throw new Error('Token inválido o expirado'); + } + } + + // buscar usuario por ID + async findUserById(userId: string): Promise { + return this.userRepository.findOne({ where: { id: userId } }); + } } diff --git a/server/apps/users/src/config/typeorm.config.ts b/server/apps/users/src/config/typeorm.config.ts index e03df39b..d598e2c5 100644 --- a/server/apps/users/src/config/typeorm.config.ts +++ b/server/apps/users/src/config/typeorm.config.ts @@ -1,12 +1,15 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm'; import * as dotenv from 'dotenv'; + dotenv.config(); +const isProduction = process.env.NODE_ENV === 'production'; + export const typeOrmConfig: TypeOrmModuleOptions = { type: 'postgres', - url: process.env.POSTGRES_URL, // Cadena de conexión - synchronize: true, // No usar en producción esto es para que migre automaticamente - entities: [__dirname + '/../**/*.entity.{ts,js}'], // Cargar todas las entidades automáticamente - migrations: [__dirname + '/../migrations/*.{ts,js}'], // Directorio de migraciones - logging: true, // Muestra las consultas SQL + url: process.env.POSTGRES_URL, + synchronize: !isProduction, // Solo habilitar en desarrollo + entities: [__dirname + '/../**/*.entity.{ts,js}'], + migrations: [__dirname + '/../migrations/*.{ts,js}'], + logging: !isProduction, // Solo mostrar logs en desarrollo };