diff --git a/src/common/decorators/user.decorator.ts b/src/common/decorators/user.decorator.ts index 2053278..77cfe8c 100755 --- a/src/common/decorators/user.decorator.ts +++ b/src/common/decorators/user.decorator.ts @@ -21,3 +21,16 @@ export const UserDec = createParamDecorator((data: keyof ITokenPayload | undefin // const req = ctx.switchToHttp().getRequest(); // return req.user; // }); + +export const UserIpAndHeaders = createParamDecorator((_data: unknown, ctx: ExecutionContext) => { + const req = ctx.switchToHttp().getRequest(); + const user = req.user; + + return { ip: req.ip, headers: req.headers, userId: user?.id }; +}); + +export interface IUserIpAndHeaders { + ip: string; + headers: Record; + userId: string; +} diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 91cf7db..349676c 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -151,6 +151,7 @@ export const enum CategoryMessage { ORDER_SHOULD_BE_INT = "ترتیب دسته بندی باید یک عدد صحیح باشد", CREATION_FAILED = "ایجاد دسته بندی موفیقیت آمیز نبود", UPDATE_FAILED = "به روز رسانی دسته بندی موفیقیت آمیز نبود", + CATEGORY_HAS_SERVICES = "دسته بندی دارای سرویس می‌باشد", } export const enum ServiceMessage { diff --git a/src/configs/typeorm.config.ts b/src/configs/typeorm.config.ts index 9391ab2..8d8378f 100755 --- a/src/configs/typeorm.config.ts +++ b/src/configs/typeorm.config.ts @@ -13,7 +13,7 @@ export function databaseConfigs(): TypeOrmModuleAsyncOptions { username: configService.getOrThrow("DB_USER"), password: configService.getOrThrow("DB_PASS"), autoLoadEntities: true, - synchronize: configService.getOrThrow("NODE_ENV") == "production" ? false : false, + synchronize: configService.getOrThrow("NODE_ENV") == "production" ? false : true, logging: configService.getOrThrow("NODE_ENV") == "production" ? false : true, migrationsTableName: "typeorm_migrations", migrationsRun: false, diff --git a/src/modules/access-logs/DTO/create-access-log.dto.ts b/src/modules/access-logs/DTO/create-access-log.dto.ts deleted file mode 100644 index 567fcbb..0000000 --- a/src/modules/access-logs/DTO/create-access-log.dto.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { IsEnum, IsNumber, IsObject, IsOptional, IsString, IsUUID } from "class-validator"; - -import { User } from "../../users/entities/user.entity"; -import { OperationType } from "../enums/operation-type.enum"; -import { UserType } from "../enums/user-type.enum"; - -export class CreateAccessLogDto { - @IsEnum(OperationType) - operationType: OperationType; - - @IsEnum(UserType) - userType: UserType; - - @IsString() - entityName: string; - - @IsOptional() - @IsString() - entityId?: string; - - @IsOptional() - @IsString() - actionDescription?: string; - - @IsOptional() - @IsString() - oldValues?: string; - - @IsOptional() - @IsString() - newValues?: string; - - @IsOptional() - @IsString() - ipAddress?: string; - - @IsOptional() - @IsString() - userAgent?: string; - - @IsOptional() - @IsString() - endpoint?: string; - - @IsOptional() - @IsNumber() - statusCode?: number; - - @IsOptional() - @IsString() - errorMessage?: string; - - @IsOptional() - @IsObject() - metadata?: Record; - - @IsOptional() - @IsUUID() - user?: Pick; - - @IsOptional() - @IsString() - requestId?: string; -} diff --git a/src/modules/access-logs/entities/access-log.entity.ts b/src/modules/access-logs/entities/access-log.entity.ts index be577b6..8eba2d3 100644 --- a/src/modules/access-logs/entities/access-log.entity.ts +++ b/src/modules/access-logs/entities/access-log.entity.ts @@ -13,42 +13,42 @@ export class AccessLog extends BaseEntity { @Column({ type: "enum", enum: UserType }) userType: UserType; - @Column({ type: "varchar", length: 255 }) + @Column({ type: "varchar", length: 255, nullable: false }) entityName: string; @Column({ type: "varchar", length: 255, nullable: true }) - entityId: string; + entityId?: string; @Column({ type: "varchar", length: 255, nullable: true }) - actionDescription: string; + actionDescription?: string; @Column({ type: "text", nullable: true }) - oldValues: string; + oldValues?: string; @Column({ type: "text", nullable: true }) - newValues: string; + newValues?: string; @Column({ type: "varchar", length: 45, nullable: true }) - ipAddress: string; + ipAddress?: string; @Column({ type: "varchar", length: 500, nullable: true }) - userAgent: string; + userAgent?: string; @Column({ type: "varchar", length: 255, nullable: true }) - endpoint: string; + endpoint?: string; @Column({ type: "integer", nullable: true }) - statusCode: number; + statusCode?: number; @Column({ type: "text", nullable: true }) - errorMessage: string; + errorMessage?: string; @Column({ type: "jsonb", nullable: true }) - metadata: Record; - - @ManyToOne(() => User, { nullable: true }) - user: User; + metadata?: Record; @Column({ type: "varchar", length: 255, nullable: true }) - requestId: string; + requestId?: string; + + @ManyToOne(() => User, { nullable: true }) + user?: User; } diff --git a/src/modules/access-logs/interfaces/access-log-data.interface.ts b/src/modules/access-logs/interfaces/access-log-data.interface.ts new file mode 100644 index 0000000..9f87a5f --- /dev/null +++ b/src/modules/access-logs/interfaces/access-log-data.interface.ts @@ -0,0 +1,6 @@ +import { User } from "../../users/entities/user.entity"; +import { AccessLog } from "../entities/access-log.entity"; + +export interface IAccessLogData extends Partial> { + user?: Pick; +} diff --git a/src/modules/access-logs/interfaces/log-context.interface.ts b/src/modules/access-logs/interfaces/log-context.interface.ts deleted file mode 100644 index 27dfd5c..0000000 --- a/src/modules/access-logs/interfaces/log-context.interface.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { FastifyRequest } from "fastify"; - -import { User } from "../../users/entities/user.entity"; -import { OperationType } from "../enums/operation-type.enum"; -import { UserType } from "../enums/user-type.enum"; - -export interface ILogContext { - operationType: OperationType; - entityName: string; - entityId?: string; - actionDescription?: string; - oldValues?: any; - newValues?: any; - user?: Pick; - userType?: UserType; - metadata?: Record; - request?: FastifyRequest; - statusCode?: number; - errorMessage?: string; -} diff --git a/src/modules/access-logs/providers/access-log.service.ts b/src/modules/access-logs/providers/access-log.service.ts index 38a182b..2d2f4a7 100644 --- a/src/modules/access-logs/providers/access-log.service.ts +++ b/src/modules/access-logs/providers/access-log.service.ts @@ -1,41 +1,18 @@ import { BadRequestException, Injectable } from "@nestjs/common"; -import { FastifyRequest } from "fastify"; import { AccessLogMessage } from "../../../common/enums/message.enum"; +import { User } from "../../users/entities/user.entity"; import { AccessLogQueryDto, AccessLogStatsQueryDto, EntityIdQueryDto } from "../DTO/access-log-query.dto"; -import { CreateAccessLogDto } from "../DTO/create-access-log.dto"; import { OperationType } from "../enums/operation-type.enum"; import { UserType } from "../enums/user-type.enum"; -import { ILogContext } from "../interfaces/log-context.interface"; +import { IAccessLogData } from "../interfaces/access-log-data.interface"; import { AccessLogRepository } from "../repositories/access-log.repository"; @Injectable() export class AccessLogService { constructor(private readonly accessLogRepository: AccessLogRepository) {} - async logOperation(context: ILogContext) { - const logData: CreateAccessLogDto = { - operationType: context.operationType, - userType: context.userType || UserType.ADMIN, - entityName: context.entityName, - entityId: context.entityId, - actionDescription: context.actionDescription, - oldValues: context.oldValues ? JSON.stringify(context.oldValues) : undefined, - newValues: context.newValues ? JSON.stringify(context.newValues) : undefined, - user: context.user, - metadata: context.metadata, - statusCode: context.statusCode, - errorMessage: context.errorMessage, - }; - - // Extract request information if available - if (context.request) { - logData.ipAddress = this.getClientIp(context.request); - logData.userAgent = context.request.headers["user-agent"]; - logData.endpoint = context.request.url; - logData.requestId = context.request.headers["x-request-id"] as string; - } - + async logOperation(logData: IAccessLogData) { const accessLog = this.accessLogRepository.create({ ...logData }); await this.accessLogRepository.save(accessLog); @@ -43,81 +20,74 @@ export class AccessLogService { } //######################################################################################## - async logCreate(entityName: string, entityId: string, newValues: Record, context: Partial = {}) { + async logCreate(entityName: string, userType: UserType, logData: IAccessLogData = {}) { return this.logOperation({ + ...logData, operationType: OperationType.CREATE, - entityName, - entityId, - newValues, actionDescription: `Created ${entityName}`, - ...context, + entityName, + userType, }); } //######################################################################################## - async logUpdate( - entityName: string, - entityId: string, - oldValues: Record, - newValues: Record, - context: Partial = {}, - ) { + async logUpdate(entityName: string, entityId: string, userType: UserType, logData: IAccessLogData = {}) { return this.logOperation({ + ...logData, operationType: OperationType.UPDATE, entityName, entityId, - oldValues, - newValues, actionDescription: `Updated ${entityName}`, - ...context, + userType, }); } //######################################################################################## - async logDelete(entityName: string, entityId: string, oldValues: Record, context: Partial = {}) { + async logDelete(entityName: string, entityId: string, userType: UserType, logData: IAccessLogData = {}) { return this.logOperation({ + ...logData, operationType: OperationType.DELETE, entityName, entityId, - oldValues, actionDescription: `Deleted ${entityName}`, - ...context, + userType, }); } //######################################################################################## - async logRead(entityName: string, entityId?: string, context: Partial = {}) { + async logRead(entityName: string, entityId: string, userType: UserType, logData: IAccessLogData = {}) { return this.logOperation({ + ...logData, operationType: OperationType.READ, entityName, entityId, actionDescription: `Read ${entityName}`, - ...context, + userType, }); } //######################################################################################## - async logLogin(userId: string, context: Partial = {}) { - return await this.logOperation({ + async logLogin(userId: string, userType: UserType, logData: IAccessLogData = {}) { + return this.logOperation({ + ...logData, operationType: OperationType.LOGIN, entityName: "User", entityId: userId, - user: { id: userId }, - userType: UserType.USER, + user: { id: userId } as User, + userType, actionDescription: "User login", - ...context, }); } //######################################################################################## - async logLogout(userId: string, context: Partial = {}) { - return await this.logOperation({ + async logLogout(userId: string, userType: UserType, logData: IAccessLogData = {}) { + return this.logOperation({ + ...logData, operationType: OperationType.LOGOUT, entityName: "User", entityId: userId, - user: { id: userId }, - userType: UserType.USER, + user: { id: userId } as User, + userType, actionDescription: "User logout", - ...context, }); } @@ -127,15 +97,15 @@ export class AccessLogService { entityName: string, entityId: string, actionDescription: string, - context: Partial = {}, + logData: IAccessLogData = {}, ) { - return await this.logOperation({ + return this.logOperation({ + ...logData, operationType, entityName, entityId, actionDescription, userType: UserType.ADMIN, - ...context, }); } @@ -146,15 +116,15 @@ export class AccessLogService { entityName: string, entityId: string, actionDescription: string, - context: Partial = {}, + logData: IAccessLogData = {}, ) { return this.logOperation({ + ...logData, operationType, entityName, entityId, actionDescription, userType: UserType.USER, - ...context, }); } @@ -164,27 +134,27 @@ export class AccessLogService { entityName: string, entityId: string, actionDescription: string, - context: Partial = {}, + logData: IAccessLogData = {}, ) { return this.logOperation({ + ...logData, operationType, entityName, entityId, actionDescription, userType: UserType.SYSTEM, - ...context, }); } //######################################################################################## - async logError(operationType: OperationType, entityName: string, errorMessage: string, context: Partial = {}) { + async logError(operationType: OperationType, entityName: string, errorMessage: string, logData: IAccessLogData = {}) { return this.logOperation({ + ...logData, operationType, entityName, errorMessage, actionDescription: `Error in ${operationType} operation on ${entityName}`, statusCode: 500, - ...context, }); } //######################################################################################## @@ -230,11 +200,4 @@ export class AccessLogService { if (!accessLog) throw new BadRequestException(AccessLogMessage.ACCESS_LOG_NOT_FOUND); return { accessLog }; } - - //######################################################################################## - private getClientIp(request: FastifyRequest): string { - return ( - (request.headers["x-forwarded-for"] as string)?.split(",")[0] || (request.headers["x-real-ip"] as string) || request.ip || "unknown" - ); - } } diff --git a/src/modules/announcements/announcement.controller.ts b/src/modules/announcements/announcement.controller.ts index 7f22230..32f0d5d 100755 --- a/src/modules/announcements/announcement.controller.ts +++ b/src/modules/announcements/announcement.controller.ts @@ -7,7 +7,7 @@ import { UpdateAnnouncementDto } from "./DTO/update-announcement.dto"; import { AnnouncementService } from "./providers/announcement.service"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { PermissionsDec } from "../../common/decorators/permission.decorator"; -import { UserDec } from "../../common/decorators/user.decorator"; +import { IUserIpAndHeaders, UserDec, UserIpAndHeaders } from "../../common/decorators/user.decorator"; import { ParamDto } from "../../common/DTO/param.dto"; import { PermissionEnum } from "../users/enums/permission.enum"; @@ -19,22 +19,26 @@ export class AnnouncementController { @PermissionsDec(PermissionEnum.ANNOUNCEMENTS) @ApiProperty({ description: "Create a new announcement (admin route)" }) @Post() - create(@Body() createAnnouncementDto: CreateAnnouncementDto, @UserDec("id") userId: string) { - return this.announcementService.createAnnouncement(createAnnouncementDto, userId); + create(@Body() createAnnouncementDto: CreateAnnouncementDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { + return this.announcementService.createAnnouncement(createAnnouncementDto, userIpAndHeaders); } @PermissionsDec(PermissionEnum.ANNOUNCEMENTS) @ApiProperty({ description: "Update an announcement (admin route)" }) @Patch(":id") - update(@Param() paramDto: ParamDto, @Body() updateAnnouncementDto: UpdateAnnouncementDto, @UserDec("id") userId: string) { - return this.announcementService.updateAnnouncement(paramDto.id, updateAnnouncementDto, userId); + update( + @Param() paramDto: ParamDto, + @Body() updateAnnouncementDto: UpdateAnnouncementDto, + @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders, + ) { + return this.announcementService.updateAnnouncement(paramDto.id, updateAnnouncementDto, userIpAndHeaders); } @PermissionsDec(PermissionEnum.ANNOUNCEMENTS) @ApiProperty({ description: "Delete an announcement (admin route)" }) @Delete(":id") - delete(@Param() paramDto: ParamDto, @UserDec("id") userId: string) { - return this.announcementService.deleteAnnouncement(paramDto.id, userId); + delete(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { + return this.announcementService.deleteAnnouncement(paramDto.id, userIpAndHeaders); } @ApiProperty({ description: "Get all announcements (admin route)" }) diff --git a/src/modules/announcements/providers/announcement.service.ts b/src/modules/announcements/providers/announcement.service.ts index 76dfb66..73070e1 100755 --- a/src/modules/announcements/providers/announcement.service.ts +++ b/src/modules/announcements/providers/announcement.service.ts @@ -3,6 +3,7 @@ import { BadRequestException, Injectable } from "@nestjs/common"; import { Queue } from "bullmq"; import dayjs from "dayjs"; +import { IUserIpAndHeaders } from "../../../common/decorators/user.decorator"; import { AnnouncementMessage } from "../../../common/enums/message.enum"; import { UserType } from "../../access-logs/enums/user-type.enum"; import { AccessLogService } from "../../access-logs/providers/access-log.service"; @@ -27,7 +28,7 @@ export class AnnouncementService { /***************************************** */ - async createAnnouncement(createAnnouncementDto: CreateAnnouncementDto, userId: string) { + async createAnnouncement(createAnnouncementDto: CreateAnnouncementDto, userIpAndHeaders: IUserIpAndHeaders) { const { serviceId, userIds } = createAnnouncementDto; let isPublic = true; @@ -65,12 +66,19 @@ export class AnnouncementService { }, ); - await this.accessLogService.logCreate("Announcement", announcement.id, announcement, { - user: { id: userId }, - userType: UserType.ADMIN, + await this.accessLogService.logCreate("Announcement", UserType.ADMIN, { + entityId: announcement.id, + user: { id: userIpAndHeaders.userId }, + endpoint: "/announcements", + requestId: userIpAndHeaders.headers["x-request-id"], + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], actionDescription: `Admin created announcement: ${announcement.title}`, metadata: { serviceId: serviceId, + userIp: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], + requestId: userIpAndHeaders.headers["x-request-id"], }, }); @@ -210,7 +218,7 @@ export class AnnouncementService { } //************************************ */ - async updateAnnouncement(id: string, updateAnnouncementDto: UpdateAnnouncementDto, userId: string) { + async updateAnnouncement(id: string, updateAnnouncementDto: UpdateAnnouncementDto, userIpAndHeaders: IUserIpAndHeaders) { const announcement = await this.announcementRepository.findOneBy({ id }); if (!announcement) throw new BadRequestException(AnnouncementMessage.NOT_FOUND); @@ -255,12 +263,19 @@ export class AnnouncementService { ); } - await this.accessLogService.logUpdate("Announcement", announcement.id, announcement, { - user: { id: userId }, + await this.accessLogService.logUpdate("Announcement", announcement.id, UserType.ADMIN, { + user: { id: userIpAndHeaders.userId }, + endpoint: "/announcements", + requestId: userIpAndHeaders.headers["x-request-id"], + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], userType: UserType.ADMIN, actionDescription: `Admin updated announcement: ${announcement.title}`, metadata: { serviceId: serviceId, + userIp: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], + requestId: userIpAndHeaders.headers["x-request-id"], }, }); @@ -272,7 +287,7 @@ export class AnnouncementService { //************************************ */ - async deleteAnnouncement(id: string, userId: string) { + async deleteAnnouncement(id: string, userIpAndHeaders: IUserIpAndHeaders) { const announcement = await this.announcementRepository.findOneBy({ id }); if (!announcement) throw new BadRequestException(AnnouncementMessage.NOT_FOUND); @@ -284,12 +299,19 @@ export class AnnouncementService { await this.announcementRepository.delete(id); - await this.accessLogService.logDelete("Announcement", announcement.id, announcement, { - user: { id: userId }, + await this.accessLogService.logDelete("Announcement", announcement.id, UserType.ADMIN, { + user: { id: userIpAndHeaders.userId }, + endpoint: "/announcements", + requestId: userIpAndHeaders.headers["x-request-id"], + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], userType: UserType.ADMIN, actionDescription: `Admin deleted announcement: ${announcement.title}`, metadata: { serviceId: announcement.targetService.id, + userIp: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], + requestId: userIpAndHeaders.headers["x-request-id"], }, }); diff --git a/src/modules/blogs/blogs.controller.ts b/src/modules/blogs/blogs.controller.ts index b2cb7fc..0f024d5 100755 --- a/src/modules/blogs/blogs.controller.ts +++ b/src/modules/blogs/blogs.controller.ts @@ -1,4 +1,5 @@ -import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Query } from "@nestjs/common"; +import { CacheInterceptor, CacheTTL } from "@nestjs/cache-manager"; +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Query, UseInterceptors } from "@nestjs/common"; import { ApiOperation } from "@nestjs/swagger"; import { BlogCommentQueryDto } from "./DTO/blog-comment-query.dto"; @@ -18,7 +19,7 @@ import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { Pagination } from "../../common/decorators/pagination.decorator"; import { PermissionsDec } from "../../common/decorators/permission.decorator"; import { SkipAuth } from "../../common/decorators/skip-auth.decorator"; -import { UserDec } from "../../common/decorators/user.decorator"; +import { IUserIpAndHeaders, UserDec, UserIpAndHeaders } from "../../common/decorators/user.decorator"; import { ParamDto } from "../../common/DTO/param.dto"; import { CategoryListSearchQueryDto } from "../danak-services/DTO/category-search-query.dto"; import { PermissionEnum } from "../users/enums/permission.enum"; @@ -31,6 +32,8 @@ export class BlogsController { ) {} @ApiOperation({ summary: "Get all public and active blogs with category id" }) + @UseInterceptors(CacheInterceptor) + @CacheTTL(120 * 1000) @SkipAuth() @Get() getBlogsPublic(@Query() queryDto: BlogSearchQueryDto) { @@ -38,6 +41,8 @@ export class BlogsController { } @ApiOperation({ summary: "Get most visited and pinned blogs" }) + @UseInterceptors(CacheInterceptor) + @CacheTTL(120 * 1000) @SkipAuth() @Get("combined") getCombinedBlogs() { @@ -53,6 +58,8 @@ export class BlogsController { } @ApiOperation({ summary: "Get a blog by id" }) + @UseInterceptors(CacheInterceptor) + @CacheTTL(120 * 1000) @SkipAuth() @Get(":id") getBlog(@Param() paramDto: ParamDto, @UserDec("isAdmin") isAdmin: boolean) { @@ -60,6 +67,8 @@ export class BlogsController { } @ApiOperation({ summary: "Get a blog by slug" }) + @UseInterceptors(CacheInterceptor) + @CacheTTL(120 * 1000) @SkipAuth() @Get("slug/:slug") getBlogBySlug(@Param() paramDto: BlogSlugDto, @UserDec("isAdmin") isAdmin: boolean) { @@ -67,6 +76,8 @@ export class BlogsController { } @ApiOperation({ summary: "Get most visited blogs" }) + @UseInterceptors(CacheInterceptor) + @CacheTTL(120 * 1000) @SkipAuth() @Get("most-visited") getMostVisitedBlogs() { @@ -82,6 +93,8 @@ export class BlogsController { } @ApiOperation({ summary: "Get most pinned blogs" }) + @UseInterceptors(CacheInterceptor) + @CacheTTL(120 * 1000) @SkipAuth() @Get("pinned") getPinnedBlogs() { @@ -100,24 +113,24 @@ export class BlogsController { @Post() @AdminRoute() @PermissionsDec(PermissionEnum.BLOGS) - createBlog(@Body() createBlogDto: CreateBlogDto, @UserDec("id") userId: string) { - return this.blogsService.createBlog(createBlogDto, userId); + createBlog(@Body() createBlogDto: CreateBlogDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { + return this.blogsService.createBlog(createBlogDto, userIpAndHeaders); } @ApiOperation({ summary: "Update a blog (admin route)" }) @Patch(":id") @AdminRoute() @PermissionsDec(PermissionEnum.BLOGS) - updateBlog(@Param() paramDto: ParamDto, @Body() updateBlogDto: UpdateBlogDto, @UserDec("id") userId: string) { - return this.blogsService.updateBlog(paramDto.id, updateBlogDto, userId); + updateBlog(@Param() paramDto: ParamDto, @Body() updateBlogDto: UpdateBlogDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { + return this.blogsService.updateBlog(paramDto.id, updateBlogDto, userIpAndHeaders); } @ApiOperation({ summary: "Delete a blog (admin route) " }) @Delete(":id") @AdminRoute() @PermissionsDec(PermissionEnum.BLOGS) - deleteBlog(@Param() paramDto: ParamDto, @UserDec("id") userId: string) { - return this.blogsService.deleteBlog(paramDto.id, userId); + deleteBlog(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { + return this.blogsService.deleteBlog(paramDto.id, userIpAndHeaders); } //-------------- comments -------------- @@ -200,15 +213,15 @@ export class BlogsController { @Delete("categories/:id") @AdminRoute() @PermissionsDec(PermissionEnum.BLOGS) - deleteCategory(@Param() paramDto: ParamDto, @UserDec("id") userId: string) { - return this.blogsService.deleteCategory(paramDto.id, userId); + deleteCategory(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { + return this.blogsService.deleteCategory(paramDto.id, userIpAndHeaders); } @ApiOperation({ summary: "toggle status of categories (admin route)" }) @PermissionsDec(PermissionEnum.BLOGS) @HttpCode(HttpStatus.OK) @Post("categories/toggle-status/:id") - toggleCategoryStatus(@Param() paramDto: ParamDto) { - return this.blogsService.toggleCategoryStatus(paramDto.id); + toggleCategoryStatus(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { + return this.blogsService.toggleCategoryStatus(paramDto.id, userIpAndHeaders); } } diff --git a/src/modules/blogs/providers/blogs.service.ts b/src/modules/blogs/providers/blogs.service.ts index 9583ae4..b3b6c34 100755 --- a/src/modules/blogs/providers/blogs.service.ts +++ b/src/modules/blogs/providers/blogs.service.ts @@ -1,6 +1,7 @@ import { BadRequestException, Injectable } from "@nestjs/common"; import { Not } from "typeorm"; +import { IUserIpAndHeaders } from "../../../common/decorators/user.decorator"; import { AdminMessage, BlogMessage, CommonMessage } from "../../../common/enums/message.enum"; import { UserType } from "../../access-logs/enums/user-type.enum"; import { AccessLogService } from "../../access-logs/providers/access-log.service"; @@ -49,13 +50,13 @@ export class BlogsService { return { message: CommonMessage.UPDATE_SUCCESS, - category, + categoryId: category.id, }; } //*********************************** */ - async deleteCategory(id: string, userId: string) { - const isSuperAdmin = await this.usersService.isSuperAdmin(userId); + async deleteCategory(id: string, userIpAndHeaders: IUserIpAndHeaders) { + const isSuperAdmin = await this.usersService.isSuperAdmin(userIpAndHeaders.userId); if (!isSuperAdmin) throw new BadRequestException(AdminMessage.NOT_ALLOWED); const category = await this.blogCategoriesRepository.findOneBy({ id }); @@ -65,23 +66,45 @@ export class BlogsService { if (blogs.length > 0) throw new BadRequestException(BlogMessage.CATEGORY_HAS_BLOGS); await this.blogCategoriesRepository.remove(category); + + await this.accessLogService.logDelete("BlogCategory", category.id, UserType.ADMIN, { + user: { id: userIpAndHeaders.userId }, + endpoint: "/blogs/categories", + requestId: userIpAndHeaders.headers["x-request-id"], + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], + actionDescription: `Admin deleted blog category: ${category.title}`, + metadata: { + categoryId: category.id, + }, + }); return { message: CommonMessage.DELETED, }; } //*********************************** */ - async toggleCategoryStatus(id: string) { + async toggleCategoryStatus(id: string, userIpAndHeaders: IUserIpAndHeaders) { const category = await this.findCategoryById(id); // category.isActive = !category.isActive; await this.blogCategoriesRepository.save(category); - // + await this.accessLogService.logUpdate("BlogCategory", category.id, UserType.ADMIN, { + user: { id: userIpAndHeaders.userId }, + endpoint: "/blogs/categories", + requestId: userIpAndHeaders.headers["x-request-id"], + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], + actionDescription: `Admin updated blog category status: ${category.title}`, + metadata: { + categoryId: category.id, + }, + }); return { message: CommonMessage.UPDATE_SUCCESS, - category, + categoryId: category.id, }; } //*********************************** */ @@ -104,7 +127,7 @@ export class BlogsService { } //*********************************** */ - async createBlog(createBlogDto: CreateBlogDto, authorId: string) { + async createBlog(createBlogDto: CreateBlogDto, userIpAndHeaders: IUserIpAndHeaders) { const existingBlog = await this.blogsRepository.findOne({ where: { title: createBlogDto.title } }); if (existingBlog) throw new BadRequestException(BlogMessage.BLOG_ALREADY_EXISTS); @@ -117,38 +140,36 @@ export class BlogsService { const blog = this.blogsRepository.create({ ...createBlogDto, - author: { id: authorId }, + author: { id: userIpAndHeaders.userId }, category, slug, }); await this.blogsRepository.save(blog); - await this.accessLogService.logCreate( - "Blog", - blog.id, - { blog: createBlogDto.title }, - { - user: { id: authorId }, - userType: UserType.ADMIN, - actionDescription: `Admin created blog: ${createBlogDto.title}`, - metadata: { - categoryId: createBlogDto.categoryId, - slug: slug, - authorId: authorId, - }, + await this.accessLogService.logCreate("Blog", UserType.ADMIN, { + user: { id: userIpAndHeaders.userId }, + endpoint: "/blogs", + requestId: userIpAndHeaders.headers["x-request-id"], + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], + actionDescription: `Admin created blog: ${createBlogDto.title}`, + metadata: { + categoryId: createBlogDto.categoryId, + slug: slug, + authorId: userIpAndHeaders.userId, }, - ); + }); return { message: CommonMessage.CREATED, - blog, + blogId: blog.id, }; } //*********************************** */ - async updateBlog(id: string, updateBlogDto: UpdateBlogDto, userId: string) { + async updateBlog(id: string, updateBlogDto: UpdateBlogDto, userIpAndHeaders: IUserIpAndHeaders) { const blog = await this.findBlogById(id); if (updateBlogDto.title) { @@ -172,30 +193,29 @@ export class BlogsService { slug: this.generateSlug(updateBlogDto.slug ?? blog.slug), }); - await this.accessLogService.logUpdate( - "Blog", - blog.id, - { blog: blog.title }, - { blog: updateBlogDto.title }, - { - user: { id: userId }, - userType: UserType.ADMIN, - actionDescription: `Admin updated blog: ${blog.title}`, - metadata: { - categoryId: updateBlogDto.categoryId, - }, + await this.accessLogService.logUpdate("Blog", blog.id, UserType.ADMIN, { + user: { id: userIpAndHeaders.userId }, + endpoint: "/blogs", + requestId: userIpAndHeaders.headers["x-request-id"], + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], + actionDescription: `Admin updated blog: ${blog.title}`, + metadata: { + categoryId: updateBlogDto.categoryId, }, - ); + oldValues: JSON.stringify({ ...blog }), + newValues: JSON.stringify({ ...updateBlogDto }), + }); return { message: CommonMessage.UPDATE_SUCCESS, - blog, + blogId: blog.id, }; } //*********************************** */ - async deleteBlog(id: string, userId: string) { - const isSuperAdmin = await this.usersService.isSuperAdmin(userId); + async deleteBlog(id: string, userIpAndHeaders: IUserIpAndHeaders) { + const isSuperAdmin = await this.usersService.isSuperAdmin(userIpAndHeaders.userId); if (!isSuperAdmin) throw new BadRequestException(AdminMessage.NOT_ALLOWED); const blog = await this.blogsRepository.findOneBy({ id }); @@ -203,19 +223,19 @@ export class BlogsService { await this.blogsRepository.softDelete({ id }); - await this.accessLogService.logDelete( - "Blog", - blog.id, - { blog: blog.title }, - { - user: { id: userId }, - userType: UserType.ADMIN, - actionDescription: `Admin deleted blog: ${blog.title}`, - metadata: { - categoryId: blog.category.id, - }, + await this.accessLogService.logDelete("Blog", blog.id, UserType.ADMIN, { + user: { id: userIpAndHeaders.userId }, + endpoint: "/blogs", + requestId: userIpAndHeaders.headers["x-request-id"], + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], + actionDescription: `Admin deleted blog: ${blog.title}`, + metadata: { + categoryId: blog.category.id, }, - ); + oldValues: JSON.stringify({ ...blog }), + newValues: JSON.stringify({ ...blog }), + }); return { message: CommonMessage.DELETED, }; diff --git a/src/modules/danak-services/danak-services.controller.ts b/src/modules/danak-services/danak-services.controller.ts index b92affe..fced3e5 100755 --- a/src/modules/danak-services/danak-services.controller.ts +++ b/src/modules/danak-services/danak-services.controller.ts @@ -23,7 +23,7 @@ import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { Pagination } from "../../common/decorators/pagination.decorator"; import { PermissionsDec } from "../../common/decorators/permission.decorator"; import { StrictRateLimit } from "../../common/decorators/rate-limit.decorator"; -import { UserDec } from "../../common/decorators/user.decorator"; +import { IUserIpAndHeaders, UserDec, UserIpAndHeaders } from "../../common/decorators/user.decorator"; import { ParamDto } from "../../common/DTO/param.dto"; import { PermissionEnum } from "../users/enums/permission.enum"; @@ -40,8 +40,8 @@ export class DanakServicesController { @PermissionsDec(PermissionEnum.SERVICES) @ApiOperation({ summary: "Create a new service category => admin route" }) @Post("categories") - createCategory(@Body() createDto: CreateDanakServiceCategoryDto) { - return this.danakServicesService.createCategory(createDto); + createCategory(@Body() createDto: CreateDanakServiceCategoryDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { + return this.danakServicesService.createCategory(createDto, userIpAndHeaders); } @AuthGuards() @@ -80,16 +80,20 @@ export class DanakServicesController { @PermissionsDec(PermissionEnum.SERVICES) @ApiOperation({ summary: "update category by id => admin route" }) @Patch("categories/:id") - updateCategory(@Param() paramDto: ParamDto, @Body() updateDto: UpdateDanakServiceCategoryDto) { - return this.danakServicesService.updateCategory(paramDto.id, updateDto); + updateCategory( + @Param() paramDto: ParamDto, + @Body() updateDto: UpdateDanakServiceCategoryDto, + @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders, + ) { + return this.danakServicesService.updateCategory(paramDto.id, updateDto, userIpAndHeaders); } @AuthGuards() @PermissionsDec(PermissionEnum.SERVICES) @ApiOperation({ summary: "delete category by id => admin route" }) @Delete("categories/:id") - deleteCategoryById(@Param() paramDto: ParamDto) { - return this.danakServicesService.deleteCategoryById(paramDto.id); + deleteCategoryById(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { + return this.danakServicesService.deleteCategoryById(paramDto.id, userIpAndHeaders); } @AuthGuards() @@ -97,16 +101,16 @@ export class DanakServicesController { @ApiOperation({ summary: "toggle status of categories => admin route" }) @HttpCode(HttpStatus.OK) @Post("categories/toggle-status/:id") - toggleCategoryStatus(@Param() paramDto: ParamDto) { - return this.danakServicesService.toggleCategoryStatus(paramDto); + toggleCategoryStatus(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { + return this.danakServicesService.toggleCategoryStatus(paramDto, userIpAndHeaders); } //-------------------- service management -------------------------- @AuthGuards() @PermissionsDec(PermissionEnum.SERVICES) @ApiOperation({ summary: "create new danak services => admin route" }) @Post() - createService(@Body() createDto: CreateServiceDto, @UserDec("id") userId: string) { - return this.danakServicesService.createService(createDto, userId); + createService(@Body() createDto: CreateServiceDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { + return this.danakServicesService.createService(createDto, userIpAndHeaders); } @AuthGuards() @@ -136,8 +140,8 @@ export class DanakServicesController { @ApiOperation({ summary: "toggle status of danak service => admin route" }) @HttpCode(HttpStatus.OK) @Post("toggle-status/:id") - toggleServiceStatus(@Param() paramDto: ParamDto) { - return this.danakServicesService.toggleServiceStatus(paramDto); + toggleServiceStatus(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { + return this.danakServicesService.toggleServiceStatus(paramDto, userIpAndHeaders); } @AuthGuards() @@ -190,8 +194,12 @@ export class DanakServicesController { @AuthGuards() @PermissionsDec(PermissionEnum.SERVICES) @Patch(":id") - updateDanakService(@Param() paramDto: ParamDto, @Body() updateDto: UpdateServiceDto, @UserDec("id") userId: string) { - return this.danakServicesService.updateDanakService(paramDto.id, updateDto, userId); + updateDanakService( + @Param() paramDto: ParamDto, + @Body() updateDto: UpdateServiceDto, + @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders, + ) { + return this.danakServicesService.updateDanakService(paramDto.id, updateDto, userIpAndHeaders); } @ApiOperation({ summary: "delete danak service" }) diff --git a/src/modules/danak-services/providers/danak-services.service.ts b/src/modules/danak-services/providers/danak-services.service.ts index 90d316b..96e4cb5 100755 --- a/src/modules/danak-services/providers/danak-services.service.ts +++ b/src/modules/danak-services/providers/danak-services.service.ts @@ -1,6 +1,7 @@ import { BadRequestException, Injectable, Logger } from "@nestjs/common"; import { DataSource, FindOptionsWhere, In, IsNull, LessThan, MoreThan, Not, QueryRunner } from "typeorm"; +import { IUserIpAndHeaders } from "../../../common/decorators/user.decorator"; import { ParamDto } from "../../../common/DTO/param.dto"; import { CategoryMessage, CommonMessage, ServiceMessage } from "../../../common/enums/message.enum"; import { UserType } from "../../access-logs/enums/user-type.enum"; @@ -48,7 +49,7 @@ export class DanakServicesService { ) {} /******************************************** */ - async createCategory(createDto: CreateDanakServiceCategoryDto) { + async createCategory(createDto: CreateDanakServiceCategoryDto, userIpAndHeaders: IUserIpAndHeaders) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); @@ -68,6 +69,31 @@ export class DanakServicesService { await this.handleOrderCollision(queryRunner, category); await queryRunner.commitTransaction(); + + await this.accessLogService.logCreate("DanakServiceCategory", UserType.ADMIN, { + user: { id: userIpAndHeaders.userId }, + endpoint: "/danak-services/categories", + requestId: userIpAndHeaders.headers["x-request-id"], + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], + actionDescription: `Admin created category: ${createDto.title}`, + metadata: { + userIp: userIpAndHeaders.ip, + referer: userIpAndHeaders.headers["referer"] || userIpAndHeaders.headers["referrer"], + userAgent: userIpAndHeaders.headers["user-agent"], + acceptLanguage: userIpAndHeaders.headers["accept-language"], + acceptEncoding: userIpAndHeaders.headers["accept-encoding"], + contentType: userIpAndHeaders.headers["content-type"], + origin: userIpAndHeaders.headers["origin"], + host: userIpAndHeaders.headers["host"], + xForwardedFor: userIpAndHeaders.headers["x-forwarded-for"], + xRealIp: userIpAndHeaders.headers["x-real-ip"], + timestamp: new Date().toISOString(), + platform: userIpAndHeaders.headers["sec-ch-ua-platform"], + mobile: userIpAndHeaders.headers["sec-ch-ua-mobile"], + }, + }); + return { message: CommonMessage.CREATED, category, @@ -112,7 +138,7 @@ export class DanakServicesService { /******************************************** */ - async updateCategory(categoryId: string, updateDto: UpdateDanakServiceCategoryDto) { + async updateCategory(categoryId: string, updateDto: UpdateDanakServiceCategoryDto, userIpAndHeaders: IUserIpAndHeaders) { const queryRunner = this.dataSource.createQueryRunner(); try { @@ -135,8 +161,35 @@ export class DanakServicesService { } await queryRunner.commitTransaction(); + + await this.accessLogService.logUpdate("DanakServiceCategory", category.id, UserType.ADMIN, { + user: { id: userIpAndHeaders.userId }, + endpoint: "/danak-services/categories", + metadata: { + userIp: userIpAndHeaders.ip, + referer: userIpAndHeaders.headers["referer"] || userIpAndHeaders.headers["referrer"], + userAgent: userIpAndHeaders.headers["user-agent"], + acceptLanguage: userIpAndHeaders.headers["accept-language"], + acceptEncoding: userIpAndHeaders.headers["accept-encoding"], + contentType: userIpAndHeaders.headers["content-type"], + origin: userIpAndHeaders.headers["origin"], + host: userIpAndHeaders.headers["host"], + xForwardedFor: userIpAndHeaders.headers["x-forwarded-for"], + xRealIp: userIpAndHeaders.headers["x-real-ip"], + timestamp: new Date().toISOString(), + platform: userIpAndHeaders.headers["sec-ch-ua-platform"], + mobile: userIpAndHeaders.headers["sec-ch-ua-mobile"], + }, + requestId: userIpAndHeaders.headers["x-request-id"], + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], + actionDescription: `Admin updated category: ${updateDto.title}`, + oldValues: JSON.stringify({ ...category }), + newValues: JSON.stringify({ ...updateDto }), + }); return { message: CommonMessage.UPDATE_SUCCESS, + categoryId: category.id, }; } catch (error) { await queryRunner.rollbackTransaction(); @@ -149,11 +202,37 @@ export class DanakServicesService { /******************************************** */ - async deleteCategoryById(categoryId: string) { + async deleteCategoryById(categoryId: string, userIpAndHeaders: IUserIpAndHeaders) { const category = await this.danakServicesCategoryRepository.findOneById(categoryId); if (!category) throw new BadRequestException(CategoryMessage.CATEGORY_NOT_EXIST); + + if (category.danakServices.length > 0) throw new BadRequestException(CategoryMessage.CATEGORY_HAS_SERVICES); // await this.danakServicesCategoryRepository.softDelete(categoryId); + + await this.accessLogService.logDelete("DanakServiceCategory", categoryId, UserType.ADMIN, { + user: { id: userIpAndHeaders.userId }, + endpoint: "/danak-services/categories", + requestId: userIpAndHeaders.headers["x-request-id"], + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], + actionDescription: `Admin deleted category: ${category.title}`, + metadata: { + userIp: userIpAndHeaders.ip, + referer: userIpAndHeaders.headers["referer"] || userIpAndHeaders.headers["referrer"], + userAgent: userIpAndHeaders.headers["user-agent"], + acceptLanguage: userIpAndHeaders.headers["accept-language"], + acceptEncoding: userIpAndHeaders.headers["accept-encoding"], + contentType: userIpAndHeaders.headers["content-type"], + origin: userIpAndHeaders.headers["origin"], + host: userIpAndHeaders.headers["host"], + xForwardedFor: userIpAndHeaders.headers["x-forwarded-for"], + xRealIp: userIpAndHeaders.headers["x-real-ip"], + timestamp: new Date().toISOString(), + platform: userIpAndHeaders.headers["sec-ch-ua-platform"], + mobile: userIpAndHeaders.headers["sec-ch-ua-mobile"], + }, + }); return { message: CommonMessage.DELETED, }; @@ -235,7 +314,7 @@ export class DanakServicesService { /******************************************** */ - async toggleCategoryStatus(paramDto: ParamDto) { + async toggleCategoryStatus(paramDto: ParamDto, userIpAndHeaders: IUserIpAndHeaders) { const category = await this.danakServicesCategoryRepository.findOneById(paramDto.id); if (!category) throw new BadRequestException(CategoryMessage.CATEGORY_NOT_EXIST); // @@ -243,14 +322,23 @@ export class DanakServicesService { // await this.danakServicesCategoryRepository.save(category); // + await this.accessLogService.logUpdate("DanakServiceCategory", category.id, UserType.ADMIN, { + user: { id: userIpAndHeaders.userId }, + endpoint: "/danak-services/categories", + requestId: userIpAndHeaders.headers["x-request-id"], + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], + }); + // return { + categoryId: category.id, message: CommonMessage.UPDATE_SUCCESS, isActive: category.isActive, }; } /******************************************** */ - async toggleServiceStatus(paramDto: ParamDto) { + async toggleServiceStatus(paramDto: ParamDto, userIpAndHeaders: IUserIpAndHeaders) { const service = await this.danakServicesRepository.findServiceById(paramDto.id); if (!service) throw new BadRequestException(ServiceMessage.SERVICE_NOT_EXIST); // @@ -258,6 +346,16 @@ export class DanakServicesService { // await this.danakServicesRepository.save(service); // + await this.accessLogService.logUpdate("DanakService", service.id, UserType.ADMIN, { + userType: UserType.ADMIN, + user: { id: userIpAndHeaders.userId }, + endpoint: "/danak-services", + requestId: userIpAndHeaders.headers["x-request-id"], + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], + actionDescription: `Admin updated service status: ${service.name}`, + }); + // return { message: CommonMessage.UPDATE_SUCCESS, isActive: service.isActive, @@ -278,7 +376,7 @@ export class DanakServicesService { }; } /******************************************** */ - async createService(createDto: CreateServiceDto, userId: string) { + async createService(createDto: CreateServiceDto, userIpAndHeaders: IUserIpAndHeaders) { const category = await this.danakServicesCategoryRepository.findOneById(createDto.categoryId); if (!category) throw new BadRequestException(CategoryMessage.CATEGORY_NOT_EXIST); @@ -295,7 +393,7 @@ export class DanakServicesService { if (createDto.audios) { audios = createDto.audios.map((audio) => { - return { audioUrl: audio, createBy: { id: userId } as User }; + return { audioUrl: audio, createBy: { id: userIpAndHeaders.userId } as User }; }); } @@ -303,7 +401,7 @@ export class DanakServicesService { if (createDto.videos) { videos = createDto.videos.map((video) => { - return { videoUrl: video, createBy: { id: userId } as User }; + return { videoUrl: video, createBy: { id: userIpAndHeaders.userId } as User }; }); } @@ -311,9 +409,12 @@ export class DanakServicesService { await this.danakServicesRepository.save(service); // Log the service creation - await this.accessLogService.logCreate("DanakService", service.id, createDto, { - user: { id: userId }, - userType: UserType.ADMIN, + await this.accessLogService.logCreate("DanakService", UserType.ADMIN, { + user: { id: userIpAndHeaders.userId }, + endpoint: "/danak-services", + requestId: userIpAndHeaders.headers["x-request-id"], + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], actionDescription: `Admin created service: ${createDto.name}`, metadata: { categoryId: createDto.categoryId, @@ -326,11 +427,11 @@ export class DanakServicesService { return { message: CommonMessage.CREATED, - service, + serviceId: service.id, }; } /******************************************** */ - async updateDanakService(serviceId: string, updateDto: UpdateServiceDto, userId: string) { + async updateDanakService(serviceId: string, updateDto: UpdateServiceDto, userIpAndHeaders: IUserIpAndHeaders) { const service = await this.findServiceById(serviceId); let images: DanakServiceImage[] = []; @@ -370,7 +471,7 @@ export class DanakServicesService { return this.danakServicesAudioRepository.create({ audioUrl: audio, danakService: service, - createBy: { id: userId } as User, + createBy: { id: userIpAndHeaders.userId }, }); }); await this.danakServicesAudioRepository.save(newAudios); @@ -385,7 +486,7 @@ export class DanakServicesService { return this.danakServicesVideoRepository.create({ videoUrl: video, danakService: service, - createBy: { id: userId } as User, + createBy: { id: userIpAndHeaders.userId }, }); }); @@ -407,9 +508,29 @@ export class DanakServicesService { audios: audios.length ? audios : service.audios, videos: videos.length ? videos : service.videos, }); + + await this.accessLogService.logUpdate("DanakService", serviceId, UserType.ADMIN, { + user: { id: userIpAndHeaders.userId }, + endpoint: "/danak-services", + requestId: userIpAndHeaders.headers["x-request-id"], + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], + actionDescription: `Admin updated service: ${service.name}`, + metadata: { + categoryId: updateDto.categoryId, + slug: updateDto.slug, + hasImages: updateDto.images && updateDto.images.length > 0, + hasAudios: updateDto.audios && updateDto.audios.length > 0, + hasVideos: updateDto.videos && updateDto.videos.length > 0, + }, + + oldValues: JSON.stringify({ ...service }), + newValues: JSON.stringify({ ...updateDto }), + }); + return { message: CommonMessage.UPDATE_SUCCESS, - service, + serviceId: service.id, }; } /******************************************** */ diff --git a/src/modules/danak-services/repositories/danak-services-category.repository.ts b/src/modules/danak-services/repositories/danak-services-category.repository.ts index 23c36c7..1bc7c6b 100755 --- a/src/modules/danak-services/repositories/danak-services-category.repository.ts +++ b/src/modules/danak-services/repositories/danak-services-category.repository.ts @@ -15,6 +15,6 @@ export class DanakServicesCategoryRepository extends Repository { - return this.findOneBy({ id, deletedAt: IsNull() }); + return this.findOne({ where: { id, deletedAt: IsNull() }, relations: { danakServices: true } }); } } diff --git a/src/modules/invoices/invoices.controller.ts b/src/modules/invoices/invoices.controller.ts index c8b3cec..986b277 100755 --- a/src/modules/invoices/invoices.controller.ts +++ b/src/modules/invoices/invoices.controller.ts @@ -10,7 +10,7 @@ import { ApiKeyGuards } from "../../common/decorators/api-key-guard.decorator"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { PermissionsDec } from "../../common/decorators/permission.decorator"; import { SkipAuth } from "../../common/decorators/skip-auth.decorator"; -import { UserDec } from "../../common/decorators/user.decorator"; +import { IUserIpAndHeaders, UserDec, UserIpAndHeaders } from "../../common/decorators/user.decorator"; import { ParamDto } from "../../common/DTO/param.dto"; import { VerifyOtpWithUserId } from "../auth/DTO/verify-otp.dto"; import { PermissionEnum } from "../users/enums/permission.enum"; @@ -23,22 +23,22 @@ export class InvoicesController { @ApiOperation({ summary: "create an invoice ==> admin route" }) @PermissionsDec(PermissionEnum.INVOICES) @Post() - createInvoice(@Body() createDto: CreateInvoiceDto, @UserDec("id") userId: string) { - return this.invoiceService.createInvoiceAdmin(createDto, userId); + createInvoice(@Body() createDto: CreateInvoiceDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { + return this.invoiceService.createInvoiceAdmin(createDto, userIpAndHeaders); } @ApiOperation({ summary: "update an invoice ==> admin route" }) @PermissionsDec(PermissionEnum.INVOICES) @Patch(":id") - updateInvoice(@Param() paramDto: ParamDto, @Body() updateDto: UpdateInvoiceDto, @UserDec("id") userId: string) { - return this.invoiceService.updateInvoiceAdmin(paramDto.id, updateDto, userId); + updateInvoice(@Param() paramDto: ParamDto, @Body() updateDto: UpdateInvoiceDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { + return this.invoiceService.updateInvoiceAdmin(paramDto.id, updateDto, userIpAndHeaders); } @ApiOperation({ summary: "delete an invoice ==> admin route" }) @PermissionsDec(PermissionEnum.INVOICES) @Delete(":id") - deleteInvoice(@Param() paramDto: ParamDto, @UserDec("id") userId: string) { - return this.invoiceService.deleteInvoiceAdmin(paramDto.id, userId); + deleteInvoice(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { + return this.invoiceService.deleteInvoiceAdmin(paramDto.id, userIpAndHeaders); } @ApiOperation({ summary: "get all invoices ==> admin route" }) @@ -62,20 +62,24 @@ export class InvoicesController { @ApiOperation({ summary: "approve request invoice by user" }) @Post(":id/approve/request") - approveRequestInvoiceByUser(@Param() paramDto: ParamDto, @UserDec("id") userId: string) { - return this.invoiceService.approveInvoiceRequest(paramDto.id, userId); + approveRequestInvoiceByUser(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { + return this.invoiceService.approveInvoiceRequest(paramDto.id, userIpAndHeaders); } @ApiOperation({ summary: "approve invoice by user" }) @Patch(":id/approve") - approveInvoiceByUser(@Param() paramDto: ParamDto, @UserDec("id") userId: string, @Body() verifyOtpDto: VerifyOtpWithUserId) { - return this.invoiceService.approveInvoiceByUser(paramDto.id, userId, verifyOtpDto); + approveInvoiceByUser( + @Param() paramDto: ParamDto, + @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders, + @Body() verifyOtpDto: VerifyOtpWithUserId, + ) { + return this.invoiceService.approveInvoiceByUser(paramDto.id, userIpAndHeaders, verifyOtpDto); } @ApiOperation({ summary: "pay invoice by user" }) @Post(":id/pay") - async payInvoice(@Param() paramDto: ParamDto, @UserDec("id") userId: string) { - return this.invoiceService.payInvoice(paramDto.id, userId); + async payInvoice(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { + return this.invoiceService.payInvoice(paramDto.id, userIpAndHeaders.userId, userIpAndHeaders); } @ApiOperation({ summary: "apply discount on invoice by user" }) diff --git a/src/modules/invoices/providers/invoices.service.ts b/src/modules/invoices/providers/invoices.service.ts index da994ce..be1c37e 100755 --- a/src/modules/invoices/providers/invoices.service.ts +++ b/src/modules/invoices/providers/invoices.service.ts @@ -5,6 +5,7 @@ import dayjs from "dayjs"; import Decimal from "decimal.js"; import { Between, DataSource, In, QueryRunner } from "typeorm"; +import { IUserIpAndHeaders } from "../../../common/decorators/user.decorator"; import { AuthMessage, InvoiceMessage, WalletMessage } from "../../../common/enums/message.enum"; import { OperationType } from "../../access-logs/enums/operation-type.enum"; import { UserType } from "../../access-logs/enums/user-type.enum"; @@ -56,7 +57,7 @@ export class InvoicesService { ///********************************** */ - async createInvoiceAdmin(createDto: CreateInvoiceDto, userId: string) { + async createInvoiceAdmin(createDto: CreateInvoiceDto, userIpAndHeaders: IUserIpAndHeaders) { const queryRunner = this.dataSource.createQueryRunner(); try { @@ -83,7 +84,7 @@ export class InvoicesService { const dueDate = dayjs().add(INVOICE.DUEDATE, "day").toDate(); const invoice = queryRunner.manager.create(this.invoiceRepository.target, { - user: { id: userId }, + user: { id: createDto.userId }, totalPrice: totalPrice.add(tax).toNumber(), originalPrice: totalPrice.add(tax).toNumber(), items: invoiceItems, @@ -113,9 +114,12 @@ export class InvoicesService { await this.scheduleInvoiceJobs(invoice, createDto); - await this.accessLogService.logCreate("Invoice", invoice.id, createDto, { - user: { id: userId }, - userType: UserType.ADMIN, + await this.accessLogService.logCreate("Invoice", UserType.ADMIN, { + user: { id: userIpAndHeaders.userId }, + endpoint: "/invoices", + requestId: userIpAndHeaders.headers["x-request-id"], + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], actionDescription: `Admin created invoice: ${invoice.numericId}`, metadata: { items: invoiceItems.map((item) => item.name).join(", "), @@ -141,7 +145,7 @@ export class InvoicesService { //********************************** */ - async updateInvoiceAdmin(invoiceId: string, updateDto: UpdateInvoiceDto, userId: string) { + async updateInvoiceAdmin(invoiceId: string, updateDto: UpdateInvoiceDto, userIpAndHeaders: IUserIpAndHeaders) { const queryRunner = this.dataSource.createQueryRunner(); try { @@ -204,10 +208,15 @@ export class InvoicesService { await queryRunner.commitTransaction(); - await this.accessLogService.logUpdate("Invoice", invoice.id, updateDto, { - user: { id: userId }, - userType: UserType.ADMIN, + await this.accessLogService.logUpdate("Invoice", invoice.id, UserType.ADMIN, { + user: { id: userIpAndHeaders.userId }, + endpoint: "/invoices", + requestId: userIpAndHeaders.headers["x-request-id"], + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], actionDescription: `Admin updated invoice: ${invoice.numericId}`, + oldValues: JSON.stringify({ ...invoice }), + newValues: JSON.stringify({ ...updateDto }), }); return { @@ -224,7 +233,7 @@ export class InvoicesService { //********************************** */ - async deleteInvoiceAdmin(invoiceId: string, userId: string) { + async deleteInvoiceAdmin(invoiceId: string, userIpAndHeaders: IUserIpAndHeaders) { const queryRunner = this.dataSource.createQueryRunner(); try { @@ -245,10 +254,16 @@ export class InvoicesService { await queryRunner.manager.remove(this.invoiceRepository.target, invoice); - await this.accessLogService.logDelete("Invoice", invoice.id, invoice, { - user: { id: userId }, - userType: UserType.ADMIN, + await this.accessLogService.logDelete("Invoice", invoice.id, UserType.ADMIN, { + user: { id: userIpAndHeaders.userId }, + endpoint: "/invoices", + requestId: userIpAndHeaders.headers["x-request-id"], + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], actionDescription: `Admin deleted invoice: ${invoice.numericId}`, + metadata: { + items: invoice.items.map((item) => item.name).join(", "), + }, }); await queryRunner.commitTransaction(); @@ -326,16 +341,16 @@ export class InvoicesService { //********************************** */ - async approveInvoiceRequest(invoiceId: string, userId: string) { + async approveInvoiceRequest(invoiceId: string, userIpAndHeaders: IUserIpAndHeaders) { const queryRunner = this.dataSource.createQueryRunner(); try { await queryRunner.connect(); await queryRunner.startTransaction(); - const user = await this.usersService.findOneByIdWithQueryRunner(userId, queryRunner); + const user = await this.usersService.findOneByIdWithQueryRunner(userIpAndHeaders.userId, queryRunner); - const invoice = await this.validateInvoiceForApproval(invoiceId, userId, queryRunner); + const invoice = await this.validateInvoiceForApproval(invoiceId, userIpAndHeaders.userId, queryRunner); const existCode = await this.otpService.checkExistOtp(user.phone, "INVOICE_VERIFY"); if (existCode) { @@ -363,8 +378,11 @@ export class InvoicesService { invoiceId, `user approved invoice request: ${invoice.numericId}`, { - user: { id: userId }, + user: { id: userIpAndHeaders.userId }, userType: UserType.USER, + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], + requestId: userIpAndHeaders.headers["x-request-id"], }, ); @@ -384,7 +402,7 @@ export class InvoicesService { //********************************** */ - async approveInvoiceByUser(invoiceId: string, userId: string, verifyOtpDto: VerifyOtpWithUserId) { + async approveInvoiceByUser(invoiceId: string, userIpAndHeaders: IUserIpAndHeaders, verifyOtpDto: VerifyOtpWithUserId) { const queryRunner = this.dataSource.createQueryRunner(); try { @@ -392,17 +410,17 @@ export class InvoicesService { await queryRunner.startTransaction(); const { code } = verifyOtpDto; - const user = await this.usersService.findOneByIdWithQueryRunner(userId, queryRunner); + const user = await this.usersService.findOneByIdWithQueryRunner(userIpAndHeaders.userId, queryRunner); const isValid = await this.otpService.verifyOtp(user.phone, code, "INVOICE_VERIFY"); if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP); - const invoice = await this.validateInvoiceForApproval(invoiceId, userId, queryRunner); + const invoice = await this.validateInvoiceForApproval(invoiceId, userIpAndHeaders.userId, queryRunner); invoice.status = InvoiceStatus.WAIT_PAYMENT; await queryRunner.manager.save(Invoice, invoice); - await this.notificationQueue.addApprovedInvoiceNotification(userId, { + await this.notificationQueue.addApprovedInvoiceNotification(userIpAndHeaders.userId, { invoiceId: invoice.numericId.toString(), dueDate: invoice.dueDate, createDate: invoice.createdAt, @@ -420,7 +438,10 @@ export class InvoicesService { invoiceId, `user approved invoice by user: ${invoice.numericId}`, { - user: { id: userId }, + user: { id: userIpAndHeaders.userId }, + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], + requestId: userIpAndHeaders.headers["x-request-id"], userType: UserType.USER, }, ); @@ -681,7 +702,12 @@ export class InvoicesService { //*********************************** */ - async payInvoice(invoiceId: string, userId: string, queryRunner = this.dataSource.createQueryRunner()) { + async payInvoice( + invoiceId: string, + userId: string, + userIpAndHeaders?: IUserIpAndHeaders, + queryRunner = this.dataSource.createQueryRunner(), + ) { let transactionStarted = false; try { @@ -784,10 +810,15 @@ export class InvoicesService { paidAt: invoice.paidAt, }); - await this.accessLogService.logUserAction(OperationType.UPDATE, "Invoice", invoiceId, `user paid invoice: ${invoice.numericId}`, { - user: { id: userId }, - userType: UserType.USER, - }); + if (userIpAndHeaders) { + await this.accessLogService.logUserAction(OperationType.UPDATE, "Invoice", invoiceId, `user paid invoice: ${invoice.numericId}`, { + user: { id: userIpAndHeaders.userId }, + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], + requestId: userIpAndHeaders.headers["x-request-id"], + userType: UserType.USER, + }); + } if (transactionStarted) await queryRunner.commitTransaction(); diff --git a/src/modules/invoices/queue/invoice.processor.ts b/src/modules/invoices/queue/invoice.processor.ts index e219255..54f7cf7 100755 --- a/src/modules/invoices/queue/invoice.processor.ts +++ b/src/modules/invoices/queue/invoice.processor.ts @@ -480,6 +480,6 @@ export class InvoiceProcessor extends WorkerProcessor { userSupportPlan.status = UserSupportPlanStatus.ACTIVE; await queryRunner.manager.save(UserSupportPlan, userSupportPlan); - await this.invoicesService.payInvoice(newInvoice.id, invoice.user.id, queryRunner); + await this.invoicesService.payInvoice(newInvoice.id, invoice.user.id, undefined, queryRunner); } } diff --git a/src/modules/subscriptions/providers/subscriptions.service.ts b/src/modules/subscriptions/providers/subscriptions.service.ts index e6d0cbb..57b75e0 100755 --- a/src/modules/subscriptions/providers/subscriptions.service.ts +++ b/src/modules/subscriptions/providers/subscriptions.service.ts @@ -284,7 +284,7 @@ export class SubscriptionsService { await this.addProvisioningJob(userSubscription, plan, user); if (isFree) { - await this.invoicesService.payInvoice(invoice.id, user.id, queryRunner); + await this.invoicesService.payInvoice(invoice.id, user.id, undefined, queryRunner); } await queryRunner.commitTransaction(); diff --git a/src/modules/support-plans/providers/support-plans.service.ts b/src/modules/support-plans/providers/support-plans.service.ts index 98a9bb9..147c73e 100644 --- a/src/modules/support-plans/providers/support-plans.service.ts +++ b/src/modules/support-plans/providers/support-plans.service.ts @@ -156,7 +156,7 @@ export class SupportPlansService { if (supportPlan.isFree) { userSupportPlan.status = UserSupportPlanStatus.ACTIVE; await queryRunner.manager.save(this.userSupportPlanRepo.target, userSupportPlan); - await this.invoicesService.payInvoice(invoice.id, user.id, queryRunner); + await this.invoicesService.payInvoice(invoice.id, user.id, undefined, queryRunner); } await queryRunner.commitTransaction(); @@ -256,7 +256,7 @@ export class SupportPlansService { if (newSupportPlan.isFree) { currentUserSupportPlan.status = UserSupportPlanStatus.ACTIVE; await queryRunner.manager.save(this.userSupportPlanRepo.target, currentUserSupportPlan); - await this.invoicesService.payInvoice(invoice.id, user.id, queryRunner); + await this.invoicesService.payInvoice(invoice.id, user.id, undefined, queryRunner); } await queryRunner.commitTransaction(); diff --git a/src/modules/users/providers/admins.service.ts b/src/modules/users/providers/admins.service.ts index 272736d..702e11f 100755 --- a/src/modules/users/providers/admins.service.ts +++ b/src/modules/users/providers/admins.service.ts @@ -2,6 +2,7 @@ import { BadRequestException, Injectable } from "@nestjs/common"; import slugify from "slugify"; import { DataSource, In, Not, QueryRunner } from "typeorm"; +import { IUserIpAndHeaders } from "../../../common/decorators/user.decorator"; import { AdminMessage, AdsMessage, UserMessage } from "../../../common/enums/message.enum"; import { UserType } from "../../access-logs/enums/user-type.enum"; import { AccessLogService } from "../../access-logs/providers/access-log.service"; @@ -28,7 +29,7 @@ export class AdminsService { private readonly accessLogService: AccessLogService, ) {} - async createAdmin(createDto: CreateAdminDto, userId: string) { + async createAdmin(createDto: CreateAdminDto, userIpAndHeaders: IUserIpAndHeaders) { const queryRunner = this.dataSource.createQueryRunner(); try { @@ -65,9 +66,12 @@ export class AdminsService { await queryRunner.commitTransaction(); // Log the admin creation - await this.accessLogService.logCreate("User", adminUser.id, createDto, { - user: { id: userId }, - userType: UserType.ADMIN, + await this.accessLogService.logCreate("User", UserType.ADMIN, { + user: { id: userIpAndHeaders.userId }, + endpoint: "/admins", + requestId: userIpAndHeaders.headers["x-request-id"], + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], actionDescription: `Admin created new admin: ${createDto.email}`, metadata: { roleId: createDto.roleId, @@ -90,7 +94,7 @@ export class AdminsService { } /************************************************************ */ - async updateAdmin(id: string, updateDto: UpdateAdminDto, userId: string) { + async updateAdmin(id: string, updateDto: UpdateAdminDto, userIpAndHeaders: IUserIpAndHeaders) { const queryRunner = this.dataSource.createQueryRunner(); try { @@ -136,18 +140,20 @@ export class AdminsService { await queryRunner.manager.save(this.userRepository.target, { ...admin, ...updateDto, password: admin.password }); await queryRunner.commitTransaction(); - - await this.accessLogService.logUpdate( - "User", - admin.id, - { ...admin }, - { ...admin }, - { - user: { id: userId }, - userType: UserType.ADMIN, - actionDescription: `Admin updated admin: ${admin.email}`, + await this.accessLogService.logUpdate("User", admin.id, UserType.ADMIN, { + user: { id: userIpAndHeaders.userId }, + endpoint: "/admins", + requestId: userIpAndHeaders.headers["x-request-id"], + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], + userType: UserType.ADMIN, + actionDescription: `Admin updated admin: ${admin.email}`, + metadata: { + roleId: updateDto.roleId, }, - ); + oldValues: JSON.stringify({ ...admin }), + newValues: JSON.stringify({ ...updateDto }), + }); return { message: AdminMessage.ADMIN_UPDATED, @@ -161,22 +167,25 @@ export class AdminsService { } /************************************************************ */ - async deleteAdmin(id: string, userId: string) { + async deleteAdmin(id: string, userIpAndHeaders: IUserIpAndHeaders) { const admin = await this.userRepository.findOne({ where: { id, roles: { isAdmin: true } } }); if (!admin) throw new BadRequestException(AdminMessage.ADMIN_NOT_FOUND); await this.userRepository.softDelete(id); - await this.accessLogService.logDelete( - "User", - admin.id, - { ...admin }, - { - user: { id: userId }, - userType: UserType.ADMIN, - actionDescription: `Admin deleted admin: ${admin.email}`, + await this.accessLogService.logDelete("User", admin.id, UserType.ADMIN, { + user: { id: userIpAndHeaders.userId }, + endpoint: "/admins", + requestId: userIpAndHeaders.headers["x-request-id"], + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], + actionDescription: `Admin deleted admin: ${admin.email}`, + metadata: { + roleId: admin.roles[0].id, + email: admin.email, + phone: admin.phone, }, - ); + }); return { message: AdminMessage.ADMIN_DELETED, diff --git a/src/modules/users/providers/customers.service.ts b/src/modules/users/providers/customers.service.ts index 5c68d74..ea2e11e 100755 --- a/src/modules/users/providers/customers.service.ts +++ b/src/modules/users/providers/customers.service.ts @@ -1,6 +1,7 @@ import { BadRequestException, Injectable, Logger } from "@nestjs/common"; import { DataSource, Not } from "typeorm"; +import { IUserIpAndHeaders } from "../../../common/decorators/user.decorator"; import { CommonMessage, UserMessage } from "../../../common/enums/message.enum"; import { UserType } from "../../access-logs/enums/user-type.enum"; import { AccessLogService } from "../../access-logs/providers/access-log.service"; @@ -37,7 +38,7 @@ export class CustomersService { private readonly accessLogService: AccessLogService, ) {} - async createCustomer(createDto: CreateCustomerDto) { + async createCustomer(createDto: CreateCustomerDto, userIpAndHeaders: IUserIpAndHeaders) { const queryRunner = this.dataSource.createQueryRunner(); try { @@ -73,11 +74,14 @@ export class CustomersService { await queryRunner.commitTransaction(); // Log the customer creation - await this.accessLogService.logCreate("User", user.id, createDto, { - userType: UserType.ADMIN, + await this.accessLogService.logCreate("User", UserType.ADMIN, { + user: { id: userIpAndHeaders.userId }, + endpoint: "/customers", + requestId: userIpAndHeaders.headers["x-request-id"], + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], actionDescription: `Admin created customer: ${createDto.email}`, metadata: { - createdBy: "admin", email: createDto.email, phone: createDto.phone, }, @@ -93,7 +97,7 @@ export class CustomersService { } } //************************************************ */ - async updateCustomer(customerId: string, updateDto: UpdateCustomerDto) { + async updateCustomer(customerId: string, updateDto: UpdateCustomerDto, userIpAndHeaders: IUserIpAndHeaders) { const queryRunner = this.dataSource.createQueryRunner(); try { await queryRunner.connect(); @@ -241,6 +245,21 @@ export class CustomersService { await queryRunner.commitTransaction(); + await this.accessLogService.logUpdate("User", customerId, UserType.ADMIN, { + user: { id: userIpAndHeaders.userId }, + endpoint: "/customers", + requestId: userIpAndHeaders.headers["x-request-id"], + ipAddress: userIpAndHeaders.ip, + userAgent: userIpAndHeaders.headers["user-agent"], + actionDescription: `Admin updated customer: ${customerId}`, + metadata: { + email: email, + phone: phone, + }, + oldValues: JSON.stringify({ ...user }), + newValues: JSON.stringify({ ...updateDto }), + }); + return { message: CommonMessage.UPDATE_SUCCESS, }; diff --git a/src/modules/users/providers/users.service.ts b/src/modules/users/providers/users.service.ts index e4df025..6dec136 100755 --- a/src/modules/users/providers/users.service.ts +++ b/src/modules/users/providers/users.service.ts @@ -7,8 +7,6 @@ import slugify from "slugify"; import { In, Not, QueryRunner } from "typeorm"; import { AuthMessage, CommonMessage, UserMessage } from "../../../common/enums/message.enum"; -import { UserType } from "../../access-logs/enums/user-type.enum"; -import { AccessLogService } from "../../access-logs/providers/access-log.service"; import { AddressService } from "../../address/providers/address.service"; import { RequestOtpDto } from "../../auth/DTO/request-otp.dto"; import { VerifyOtpDto } from "../../auth/DTO/verify-otp.dto"; @@ -49,7 +47,6 @@ export class UsersService { private readonly smsService: SmsService, private readonly cacheService: CacheService, private readonly referralsService: ReferralsService, - private readonly accessLogService: AccessLogService, ) {} /************************************************************ */ @@ -117,8 +114,6 @@ export class UsersService { const user = await this.userRepository.findOneBy({ id: userId }); if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); - const oldUserData = { ...user }; - if (updateProfileDto.cityId) { const { city: existCity } = await this.addressService.getCity(+updateProfileDto.cityId); if (!existCity) throw new BadRequestException(UserMessage.CITY_NOT_FOUND); @@ -132,15 +127,7 @@ export class UsersService { if (existUserName) throw new BadRequestException(UserMessage.USERNAME_EXIST); } - const updatedUser = await this.userRepository.save({ ...user, ...updateProfileDto, city }); - - // Log the profile update - await this.accessLogService.logUpdate("User", userId, oldUserData, updatedUser, { - user: { id: userId }, - userType: UserType.USER, - actionDescription: `User updated profile: ${user.email}`, - metadata: { updatedFields: Object.keys(updateProfileDto) }, - }); + await this.userRepository.save({ ...user, ...updateProfileDto, city }); return { message: CommonMessage.UPDATE_SUCCESS, diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index 8cc308a..1c8664e 100755 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -25,7 +25,7 @@ import { AdminRoute } from "../../common/decorators/admin.decorator"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { PermissionsDec } from "../../common/decorators/permission.decorator"; import { SkipAuth } from "../../common/decorators/skip-auth.decorator"; -import { UserDec } from "../../common/decorators/user.decorator"; +import { IUserIpAndHeaders, UserDec, UserIpAndHeaders } from "../../common/decorators/user.decorator"; import { ParamDto } from "../../common/DTO/param.dto"; import { RequestOtpDto } from "../auth/DTO/request-otp.dto"; import { VerifyOtpDto } from "../auth/DTO/verify-otp.dto"; @@ -136,14 +136,18 @@ export class UsersController { @ApiOperation({ summary: "Create customer" }) @HttpCode(HttpStatus.CREATED) @Post("customers") - createCustomer(@Body() createDto: CreateCustomerDto) { - return this.customersService.createCustomer(createDto); + createCustomer(@Body() createDto: CreateCustomerDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { + return this.customersService.createCustomer(createDto, userIpAndHeaders); } @ApiOperation({ summary: "Update customer" }) @Patch("customers/:id") - updateCustomer(@Param() paramDto: ParamDto, @Body() updateDto: UpdateCustomerDto) { - return this.customersService.updateCustomer(paramDto.id, updateDto); + updateCustomer( + @Param() paramDto: ParamDto, + @Body() updateDto: UpdateCustomerDto, + @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders, + ) { + return this.customersService.updateCustomer(paramDto.id, updateDto, userIpAndHeaders); } @ApiOperation({ summary: "get customers ==> admin route" }) @@ -180,22 +184,22 @@ export class UsersController { @ApiOperation({ summary: "update admin ==> admin route" }) @PermissionsDec(PermissionEnum.ADMINS, PermissionEnum.CUSTOMERS) @Patch("admins/:id") - updateAdmin(@Param() paramDto: ParamDto, @Body() updateDto: UpdateAdminDto, @UserDec("id") userId: string) { - return this.adminsService.updateAdmin(paramDto.id, updateDto, userId); + updateAdmin(@Param() paramDto: ParamDto, @Body() updateDto: UpdateAdminDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { + return this.adminsService.updateAdmin(paramDto.id, updateDto, userIpAndHeaders); } @ApiOperation({ summary: "delete admin ==> admin route" }) @PermissionsDec(PermissionEnum.ADMINS, PermissionEnum.CUSTOMERS) @Delete("admins/:id") - deleteAdmin(@Param() paramDto: ParamDto, @UserDec("id") userId: string) { - return this.adminsService.deleteAdmin(paramDto.id, userId); + deleteAdmin(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { + return this.adminsService.deleteAdmin(paramDto.id, userIpAndHeaders); } @ApiOperation({ summary: "create admin ==> admin route" }) @PermissionsDec(PermissionEnum.ADMINS, PermissionEnum.CUSTOMERS) @Post("admins") - createAdmin(@Body() createDto: CreateAdminDto, @UserDec("id") userId: string) { - return this.adminsService.createAdmin(createDto, userId); + createAdmin(@Body() createDto: CreateAdminDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { + return this.adminsService.createAdmin(createDto, userIpAndHeaders); } @ApiOperation({ summary: "get admin permissions" })