Repository: js-tasks-ru/nodejs-20250303_nickxl1138
Pull request #3: Задание 3
Author: login: NickXL1138
Created: 2025-04-01T18:45:40Z | Merged: 2025-04-03T10:41:31Z



Diff:
diff --git a/03-di/01-notification-service/app.module.ts b/03-di/01-notification-service/app.module.ts
index 3d6655f..bdd77a4 100644
--- a/03-di/01-notification-service/app.module.ts
+++ b/03-di/01-notification-service/app.module.ts
@@ -1,7 +1,16 @@
 import { Module } from "@nestjs/common";
 import { TasksModule } from "./tasks/tasks.module";
+import { ConfigModule } from "@nestjs/config";
+import configuration from "./config/configuration";
 
 @Module({
-  imports: [TasksModule],
+  imports: [
+    TasksModule,
+    ConfigModule.forRoot({
+      ignoreEnvFile: true,
+      isGlobal: true,
+      load: [configuration],
+    }),
+  ],
 })
 export class AppModule {}
diff --git a/03-di/01-notification-service/config/configuration.ts b/03-di/01-notification-service/config/configuration.ts
new file mode 100644
index 0000000..e9c1604
--- /dev/null
+++ b/03-di/01-notification-service/config/configuration.ts
@@ -0,0 +1,6 @@
+import { registerAs } from "@nestjs/config";
+
+export default registerAs("customConfig", () => ({
+  senderEmail: "test@gggmail.com",
+  smsGateway: "txt.ggg.net",
+}));
diff --git a/03-di/01-notification-service/notifications/notifications.service.ts b/03-di/01-notification-service/notifications/notifications.service.ts
index 96a5cea..39ab70f 100644
--- a/03-di/01-notification-service/notifications/notifications.service.ts
+++ b/03-di/01-notification-service/notifications/notifications.service.ts
@@ -1,4 +1,45 @@
-import { Injectable } from "@nestjs/common";
+import { BadRequestException, Injectable } from "@nestjs/common";
+import { appendFile } from "fs";
+import { ConfigService } from "@nestjs/config";
 
 @Injectable()
-export class NotificationsService {}
+export class NotificationsService {
+  constructor(private configService: ConfigService) {
+    const newSenderEmail = this.configService.get("customConfig.senderEmail");
+    const newSMSGateway = this.configService.get("customConfig.smsGateway");
+
+    console.log(newSenderEmail, newSMSGateway);
+  }
+
+  sendEmail(to: string, subject: string, message: string): void {
+    if (!to || !subject || !message) {
+      throw new BadRequestException(
+        "Отсутствуют данные для отправки Email-уведомления",
+      );
+    }
+
+    const sentEmailLog = `Email sent to ${to}: [${subject}] ${message}`;
+
+    this.sentLogToFile(sentEmailLog);
+
+    return console.log(sentEmailLog);
+  }
+
+  sendSMS(to: string, message: string): void {
+    if (!to || !message) {
+      throw new BadRequestException(
+        "Отсутствуют данные для отправки SMS-уведомления",
+      );
+    }
+
+    const sentSMSLog = `SMS sent to ${to}: ${message}`;
+
+    this.sentLogToFile(sentSMSLog);
+
+    return console.log(sentSMSLog);
+  }
+
+  sentLogToFile(log: string) {
+    return appendFile("file-log.txt", log + `\n`, () => {});
+  }
+}
diff --git a/03-di/01-notification-service/tasks/tasks.module.ts b/03-di/01-notification-service/tasks/tasks.module.ts
index adecf42..db16325 100644
--- a/03-di/01-notification-service/tasks/tasks.module.ts
+++ b/03-di/01-notification-service/tasks/tasks.module.ts
@@ -1,9 +1,11 @@
 import { Module } from "@nestjs/common";
 import { TasksController } from "./tasks.controller";
 import { TasksService } from "./tasks.service";
+import { NotificationsModule } from "../notifications/notifications.module";
+import { UsersModule } from "../users/users.module";
 
 @Module({
-  imports: [],
+  imports: [NotificationsModule, UsersModule],
   controllers: [TasksController],
   providers: [TasksService],
 })
diff --git a/03-di/01-notification-service/tasks/tasks.service.ts b/03-di/01-notification-service/tasks/tasks.service.ts
index 5c81948..ed3d4c3 100644
--- a/03-di/01-notification-service/tasks/tasks.service.ts
+++ b/03-di/01-notification-service/tasks/tasks.service.ts
@@ -1,8 +1,16 @@
-import { Injectable, NotFoundException } from "@nestjs/common";
+import { Inject, Injectable, NotFoundException } from "@nestjs/common";
 import { CreateTaskDto, Task, TaskStatus, UpdateTaskDto } from "./task.model";
+import { NotificationsService } from "../notifications/notifications.service";
+import { UsersService } from "../users/users.service";
 
 @Injectable()
 export class TasksService {
+  @Inject(NotificationsService)
+  private notificationsService: NotificationsService;
+
+  @Inject(UsersService)
+  private usersService: UsersService;
+
   private tasks: Task[] = [];
 
   constructor() {}
@@ -18,6 +26,16 @@ export class TasksService {
     };
     this.tasks.push(task);
 
+    const user = this.usersService.getUserById(assignedTo);
+    if (!user) {
+      throw new NotFoundException(`Пользователь с ID ${assignedTo} не найден`);
+    }
+
+    const subject = "Новая задача";
+    const message = `Вы назначены ответственным за задачу: "${title}"`;
+
+    this.notificationsService.sendEmail(user.email, subject, message);
+
     return task;
   }
 
@@ -28,6 +46,17 @@ export class TasksService {
     }
 
     Object.assign(task, updateTaskDto);
+
+    const { title, status, assignedTo } = task;
+    const user = this.usersService.getUserById(assignedTo);
+    if (!user) {
+      throw new NotFoundException(`Пользователь с ID ${assignedTo} не найден`);
+    }
+
+    const message = `Статус задачи "${title}" обновлён на "${status}"`;
+
+    this.notificationsService.sendSMS(user.phone, message);
+
     return task;
   }
 }
diff --git a/04-request-lifecycle/01-nestjs-components/filters/http-error.filter.ts b/04-request-lifecycle/01-nestjs-components/filters/http-error.filter.ts
index f879724..aca9b16 100644
--- a/04-request-lifecycle/01-nestjs-components/filters/http-error.filter.ts
+++ b/04-request-lifecycle/01-nestjs-components/filters/http-error.filter.ts
@@ -1,5 +1,32 @@
-import { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
+import {
+  ArgumentsHost,
+  Catch,
+  ExceptionFilter,
+  HttpException,
+  HttpStatus,
+} from "@nestjs/common";
+import { Response } from "express";
+import { appendFileSync } from "fs";
 
+@Catch()
 export class HttpErrorFilter implements ExceptionFilter {
-  catch(exception: any, host: ArgumentsHost) {}
+  catch(exception: any, host: ArgumentsHost) {
+    const response = host.switchToHttp().getResponse<Response>();
+
+    const statusCode =
+      exception instanceof HttpException
+        ? exception.getStatus()
+        : HttpStatus.INTERNAL_SERVER_ERROR;
+
+    const message = exception.message;
+    const timestamp = new Date().toISOString();
+
+    appendFileSync("errors.log", `[${timestamp}] ${statusCode} - ${message}\n`);
+
+    response.status(statusCode).json({
+      statusCode: statusCode,
+      message: message,
+      timestamp: timestamp,
+    });
+  }
 }
diff --git a/04-request-lifecycle/01-nestjs-components/guards/roles.guard.ts b/04-request-lifecycle/01-nestjs-components/guards/roles.guard.ts
index 470c45c..e19f565 100644
--- a/04-request-lifecycle/01-nestjs-components/guards/roles.guard.ts
+++ b/04-request-lifecycle/01-nestjs-components/guards/roles.guard.ts
@@ -1,5 +1,19 @@
-import { CanActivate, ExecutionContext } from "@nestjs/common";
+import {
+  CanActivate,
+  ExecutionContext,
+  ForbiddenException,
+  Injectable,
+} from "@nestjs/common";
 
+@Injectable()
 export class RolesGuard implements CanActivate {
-  canActivate(context: ExecutionContext) {}
+  canActivate(context: ExecutionContext) {
+    const xRoleHeader = context.switchToHttp().getRequest().headers["x-role"];
+
+    if (xRoleHeader !== "admin") {
+      throw new ForbiddenException("Доступ запрещён: требуется роль admin");
+    }
+
+    return true;
+  }
 }
diff --git a/04-request-lifecycle/01-nestjs-components/interceptors/api-version.interceptor.ts b/04-request-lifecycle/01-nestjs-components/interceptors/api-version.interceptor.ts
index ee49ab8..7199800 100644
--- a/04-request-lifecycle/01-nestjs-components/interceptors/api-version.interceptor.ts
+++ b/04-request-lifecycle/01-nestjs-components/interceptors/api-version.interceptor.ts
@@ -1,5 +1,21 @@
-import { NestInterceptor, ExecutionContext, CallHandler } from "@nestjs/common";
+import {
+  NestInterceptor,
+  ExecutionContext,
+  CallHandler,
+  Injectable,
+} from "@nestjs/common";
+import { map } from "rxjs";
 
+@Injectable()
 export class ApiVersionInterceptor implements NestInterceptor {
-  intercept(context: ExecutionContext, next: CallHandler) {}
+  intercept(context: ExecutionContext, next: CallHandler) {
+    const startTime = Date.now();
+    return next.handle().pipe(
+      map((data) => ({
+        ...data,
+        apiVersion: "1.0",
+        executionTime: `${Date.now() - startTime}ms`,
+      })),
+    );
+  }
 }
diff --git a/04-request-lifecycle/01-nestjs-components/main.ts b/04-request-lifecycle/01-nestjs-components/main.ts
index 72a150e..082698f 100644
--- a/04-request-lifecycle/01-nestjs-components/main.ts
+++ b/04-request-lifecycle/01-nestjs-components/main.ts
@@ -1,8 +1,14 @@
 import { NestFactory } from "@nestjs/core";
 import { AppModule } from "./app.module";
+import { ApiVersionInterceptor } from "interceptors/api-version.interceptor";
+import { HttpErrorFilter } from "filters/http-error.filter";
 
 async function bootstrap() {
   const app = await NestFactory.create(AppModule);
+
+  app.useGlobalInterceptors(new ApiVersionInterceptor());
+  app.useGlobalFilters(new HttpErrorFilter());
+
   await app.listen(process.env.PORT ?? 3000);
 }
 bootstrap();
diff --git a/04-request-lifecycle/01-nestjs-components/middlewares/logging.middleware.ts b/04-request-lifecycle/01-nestjs-components/middlewares/logging.middleware.ts
index 028f62a..c02f664 100644
--- a/04-request-lifecycle/01-nestjs-components/middlewares/logging.middleware.ts
+++ b/04-request-lifecycle/01-nestjs-components/middlewares/logging.middleware.ts
@@ -1,6 +1,10 @@
-import { NestMiddleware } from "@nestjs/common";
+import { Injectable, NestMiddleware } from "@nestjs/common";
 import { Request, Response, NextFunction } from "express";
 
+@Injectable()
 export class LoggingMiddleware implements NestMiddleware {
-  use(req: Request, res: Response, next: NextFunction) {}
+  use(req: Request, res: Response, next: NextFunction) {
+    console.log(`[${req.method}] ${req.path}`);
+    next();
+  }
 }
diff --git a/04-request-lifecycle/01-nestjs-components/pipes/parse-int.pipe.ts b/04-request-lifecycle/01-nestjs-components/pipes/parse-int.pipe.ts
index cd68a51..75d5110 100644
--- a/04-request-lifecycle/01-nestjs-components/pipes/parse-int.pipe.ts
+++ b/04-request-lifecycle/01-nestjs-components/pipes/parse-int.pipe.ts
@@ -1,5 +1,12 @@
-import { PipeTransform } from "@nestjs/common";
+import { BadRequestException, Injectable, PipeTransform } from "@nestjs/common";
 
+@Injectable()
 export class ParseIntPipe implements PipeTransform {
-  transform(value: string): number {}
+  transform(value: string): number {
+    const valueToNumber = parseInt(value, 10);
+    if (isNaN(valueToNumber)) {
+      throw new BadRequestException(`"${value}" не является числом`);
+    }
+    return valueToNumber;
+  }
 }
diff --git a/04-request-lifecycle/01-nestjs-components/tasks/tasks.controller.ts b/04-request-lifecycle/01-nestjs-components/tasks/tasks.controller.ts
index 5d0a494..ce55d25 100644
--- a/04-request-lifecycle/01-nestjs-components/tasks/tasks.controller.ts
+++ b/04-request-lifecycle/01-nestjs-components/tasks/tasks.controller.ts
@@ -7,9 +7,11 @@ import {
   ParseIntPipe,
   Patch,
   Post,
+  UseGuards,
 } from "@nestjs/common";
 import { TasksService } from "./tasks.service";
 import { CreateTaskDto, UpdateTaskDto } from "./task.model";
+import { RolesGuard } from "../guards/roles.guard";
 
 @Controller("tasks")
 export class TasksController {
@@ -26,6 +28,7 @@ export class TasksController {
   }
 
   @Post()
+  @UseGuards(RolesGuard)
   createTask(@Body() task: CreateTaskDto) {
     return this.tasksService.createTask(task);
   }
@@ -39,6 +42,7 @@ export class TasksController {
   }
 
   @Delete(":id")
+  @UseGuards(RolesGuard)
   deleteTask(@Param("id", ParseIntPipe) id: number) {
     return this.tasksService.deleteTask(id);
   }
diff --git a/04-request-lifecycle/01-nestjs-components/tasks/tasks.module.ts b/04-request-lifecycle/01-nestjs-components/tasks/tasks.module.ts
index adecf42..c936799 100644
--- a/04-request-lifecycle/01-nestjs-components/tasks/tasks.module.ts
+++ b/04-request-lifecycle/01-nestjs-components/tasks/tasks.module.ts
@@ -1,10 +1,15 @@
-import { Module } from "@nestjs/common";
+import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
 import { TasksController } from "./tasks.controller";
 import { TasksService } from "./tasks.service";
+import { LoggingMiddleware } from "../middlewares/logging.middleware";
 
 @Module({
   imports: [],
   controllers: [TasksController],
   providers: [TasksService],
 })
-export class TasksModule {}
+export class TasksModule implements NestModule {
+  configure(consumer: MiddlewareConsumer) {
+    consumer.apply(LoggingMiddleware).forRoutes("*");
+  }
+}
diff --git a/package.json b/package.json
index 4e6905f..5b120a5 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
   "dependencies": {
     "@jest/globals": "^29.7.0",
     "@nestjs/common": "^10.0.0",
+    "@nestjs/config": "^4.0.2",
     "@nestjs/core": "^10.0.0",
     "@nestjs/jwt": "^11.0.0",
     "@nestjs/mapped-types": "^2.0.6",


Reviews:
user: login: tadjik1
body: В целом задача выполнена хорошо, но есть несколько важных моментов, которые стоит исправить для улучшения качества кода и обработки ошибок.
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-04-02T09:11:16Z
user: login: tadjik1
body: Спасибо за правки, работа выполнена отлично 👍 
state: APPROVED
author_association: NONE
submitted_at: 2025-04-03T10:40:41Z

Review comments:
diff_hunk: @@ -18,6 +26,12 @@ export class TasksService {
     };
     this.tasks.push(task);
 
+    const userEmail = this.usersService.getUserById(assignedTo).email;
path: 03-di/01-notification-service/tasks/tasks.service.ts
user: login: tadjik1
body: Мне кажется, стоит проверить наличие пользователя перед доступом к его email или телефону, иначе возможна ошибка выполнения, если пользователь не найден.
created_at: 2025-04-02T09:09:48Z
author_association: NONE
diff_hunk: @@ -28,6 +42,13 @@ export class TasksService {
     }
 
     Object.assign(task, updateTaskDto);
+
+    const { title, status, assignedTo } = task;
+    const userPhone = this.usersService.getUserById(assignedTo).phone;
path: 03-di/01-notification-service/tasks/tasks.service.ts
user: login: tadjik1
body: Здесь также рекомендую добавить проверку на существование пользователя перед отправкой SMS, чтобы избежать возможных ошибок.
created_at: 2025-04-02T09:09:49Z
author_association: NONE
diff_hunk: @@ -1,4 +1,37 @@
-import { Injectable } from "@nestjs/common";
+import { BadRequestException, Injectable } from "@nestjs/common";
+import { appendFile } from "fs";
+import { ConfigService } from "@nestjs/config";
 
 @Injectable()
-export class NotificationsService {}
+export class NotificationsService {
+  constructor(private configService: ConfigService) {
+    const newSenderEmail = this.configService.get("customConfig.senderEmail");
+    const newSMSGateway = this.configService.get("customConfig.smsGateway");
+
+    console.log(newSenderEmail, newSMSGateway);
+  }
+
+  sendEmail(to: string, subject: string, message: string): void {
+    if (!to || !subject || !message) {
+      throw new BadRequestException();
path: 03-di/01-notification-service/notifications/notifications.service.ts
user: login: tadjik1
body: Было бы лучше явно указать сообщение ошибки в BadRequestException, чтобы было понятно, какие именно данные некорректны.
created_at: 2025-04-02T09:09:49Z
author_association: NONE
diff_hunk: @@ -1,4 +1,37 @@
-import { Injectable } from "@nestjs/common";
+import { BadRequestException, Injectable } from "@nestjs/common";
+import { appendFile } from "fs";
+import { ConfigService } from "@nestjs/config";
 
 @Injectable()
-export class NotificationsService {}
+export class NotificationsService {
+  constructor(private configService: ConfigService) {
+    const newSenderEmail = this.configService.get("customConfig.senderEmail");
+    const newSMSGateway = this.configService.get("customConfig.smsGateway");
+
+    console.log(newSenderEmail, newSMSGateway);
+  }
+
+  sendEmail(to: string, subject: string, message: string): void {
+    if (!to || !subject || !message) {
+      throw new BadRequestException();
+    }
+
+    const sentEmailLog = `Email sent to ${to}: [${subject}] ${message}`;
+
+    appendFile("file-log.txt", sentEmailLog + `\n`, () => {});
+
+    return console.log(sentEmailLog);
+  }
+
+  sendSMS(to: string, message: string): void {
+    if (!to || !message) {
+      throw new BadRequestException();
path: 03-di/01-notification-service/notifications/notifications.service.ts
user: login: tadjik1
body: Здесь тоже рекомендую явно указать сообщение ошибки в BadRequestException для лучшей читаемости и отладки.
created_at: 2025-04-02T09:09:49Z
author_association: NONE
diff_hunk: @@ -1,4 +1,37 @@
-import { Injectable } from "@nestjs/common";
+import { BadRequestException, Injectable } from "@nestjs/common";
+import { appendFile } from "fs";
+import { ConfigService } from "@nestjs/config";
 
 @Injectable()
-export class NotificationsService {}
+export class NotificationsService {
+  constructor(private configService: ConfigService) {
+    const newSenderEmail = this.configService.get("customConfig.senderEmail");
+    const newSMSGateway = this.configService.get("customConfig.smsGateway");
+
+    console.log(newSenderEmail, newSMSGateway);
+  }
+
+  sendEmail(to: string, subject: string, message: string): void {
+    if (!to || !subject || !message) {
+      throw new BadRequestException();
+    }
+
+    const sentEmailLog = `Email sent to ${to}: [${subject}] ${message}`;
+
+    appendFile("file-log.txt", sentEmailLog + `\n`, () => {});
path: 03-di/01-notification-service/notifications/notifications.service.ts
user: login: tadjik1
body: Мне кажется, что логирование в файл лучше вынести в отдельный метод, чтобы избежать дублирования кода и улучшить читаемость.
created_at: 2025-04-02T09:09:49Z
author_association: NONE
diff_hunk: @@ -1,4 +1,37 @@
-import { Injectable } from "@nestjs/common";
+import { BadRequestException, Injectable } from "@nestjs/common";
+import { appendFile } from "fs";
+import { ConfigService } from "@nestjs/config";
 
 @Injectable()
-export class NotificationsService {}
+export class NotificationsService {
+  constructor(private configService: ConfigService) {
+    const newSenderEmail = this.configService.get("customConfig.senderEmail");
+    const newSMSGateway = this.configService.get("customConfig.smsGateway");
+
+    console.log(newSenderEmail, newSMSGateway);
+  }
+
+  sendEmail(to: string, subject: string, message: string): void {
+    if (!to || !subject || !message) {
+      throw new BadRequestException();
+    }
+
+    const sentEmailLog = `Email sent to ${to}: [${subject}] ${message}`;
+
+    appendFile("file-log.txt", sentEmailLog + `\n`, () => {});
+
+    return console.log(sentEmailLog);
+  }
+
+  sendSMS(to: string, message: string): void {
+    if (!to || !message) {
+      throw new BadRequestException();
+    }
+
+    const sentSMSLog = `SMS sent to ${to}: ${message}`;
+
+    appendFile("file-log.txt", sentSMSLog + `\n`, () => {});
path: 03-di/01-notification-service/notifications/notifications.service.ts
user: login: tadjik1
body: Здесь аналогично, лучше вынести повторяющийся код логирования в отдельный метод.
created_at: 2025-04-02T09:09:49Z
author_association: NONE

PR comments:
user: login: jsru-1
created_at: 2025-04-01T18:47:03Z
body: Добавляю преподавателя (@tadjik1) для код-ревью.
author_association: CONTRIBUTOR
user: login: jsru-1
created_at: 2025-04-03T10:18:14Z
body: Добавляю преподавателя (@tadjik1) для код-ревью.
author_association: CONTRIBUTOR