From 3e83779cfd82f1662c5d9d035938358661544965 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Thu, 27 Mar 2025 16:35:11 +0300 Subject: [PATCH 01/43] refactor: improve role comparison logic in RoleGuard --- src/modules/auth/guard/role/role.guard.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/modules/auth/guard/role/role.guard.ts b/src/modules/auth/guard/role/role.guard.ts index da5533fb..c6dcbcbf 100644 --- a/src/modules/auth/guard/role/role.guard.ts +++ b/src/modules/auth/guard/role/role.guard.ts @@ -15,8 +15,10 @@ export class RoleGuard implements CanActivate { context.getClass(), ]); - const user: User = context.switchToHttp().getRequest().user; + const user = context.switchToHttp().getRequest().user; - return requiredRoles.some((role) => user.name === role); + console.log('user data', user.accounts[0].role.name.toUpperCase()); + + return requiredRoles.some((role) => user.accounts?.[0]?.role?.name.toLowerCase() === role.toLowerCase()); } } From 7d960249b06ec9b41b808bb1770bc663e0395b6c Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Thu, 27 Mar 2025 16:39:15 +0300 Subject: [PATCH 02/43] fix es-lint error --- src/modules/auth/guard/role/role.guard.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/modules/auth/guard/role/role.guard.ts b/src/modules/auth/guard/role/role.guard.ts index c6dcbcbf..6ab7e9b1 100644 --- a/src/modules/auth/guard/role/role.guard.ts +++ b/src/modules/auth/guard/role/role.guard.ts @@ -17,8 +17,9 @@ export class RoleGuard implements CanActivate { const user = context.switchToHttp().getRequest().user; - console.log('user data', user.accounts[0].role.name.toUpperCase()); - - return requiredRoles.some((role) => user.accounts?.[0]?.role?.name.toLowerCase() === role.toLowerCase()); + return requiredRoles.some( + (role) => + user.accounts?.[0]?.role?.name.toLowerCase() === role.toLowerCase(), + ); } } From 8dc778320b2b84cb5188826ba741ad17c43bf713 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Thu, 27 Mar 2025 16:44:05 +0300 Subject: [PATCH 03/43] add Admin role to seed data --- prisma/seed.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/prisma/seed.ts b/prisma/seed.ts index 603766b1..11bb5227 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -18,6 +18,11 @@ async function seedBatchOne() { type: RoleType.MENTOR, isDefault: true, }, + { + name: 'Admin', + type: RoleType.ADMIN, + isDefault: true, + }, ], skipDuplicates: true, // Prevent duplicate seeding }); From e7693d840d8b743728ea826f3ba844ada6ce1703 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Thu, 27 Mar 2025 16:45:47 +0300 Subject: [PATCH 04/43] refactor: remove unused User import in RoleGuard --- src/modules/auth/guard/role/role.guard.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/auth/guard/role/role.guard.ts b/src/modules/auth/guard/role/role.guard.ts index 6ab7e9b1..b9744b2c 100644 --- a/src/modules/auth/guard/role/role.guard.ts +++ b/src/modules/auth/guard/role/role.guard.ts @@ -2,7 +2,6 @@ import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { Observable } from 'rxjs'; import { ROLE_KEY } from '../../auth.decorator'; -import { User } from 'src/modules/admin/user/entities/user.entity'; @Injectable() export class RoleGuard implements CanActivate { From 6bc26f68777a08e17a86963e2da8bbdebc512ba1 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Thu, 27 Mar 2025 17:20:04 +0300 Subject: [PATCH 05/43] feat: enhance admin access by adding RoleGuard and supporting ADMIN role in Channel and Mentor controllers --- src/modules/admin/channel/channel.controller.ts | 5 +++-- src/modules/admin/mentor/mentor.controller.ts | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/modules/admin/channel/channel.controller.ts b/src/modules/admin/channel/channel.controller.ts index d8b2ca64..8ce281c3 100644 --- a/src/modules/admin/channel/channel.controller.ts +++ b/src/modules/admin/channel/channel.controller.ts @@ -16,10 +16,11 @@ import { UpdateChannelDto } from './dto/update-channel.dto'; import { GetChannelDto } from './dto/get-channel.dto'; import { Roles } from 'src/modules/auth/auth.decorator'; import { AuthGuard } from 'src/modules/auth/guard/auth/auth.guard'; +import { RoleGuard } from 'src/modules/auth/guard/role/role.guard'; @Controller('admin/channel') -@UseGuards(AuthGuard) -@Roles('OWNER') +@UseGuards(AuthGuard, RoleGuard) +@Roles('OWNER', 'ADMIN') export class ChannelController { constructor(private readonly channelService: ChannelService) {} diff --git a/src/modules/admin/mentor/mentor.controller.ts b/src/modules/admin/mentor/mentor.controller.ts index 71e1c1b5..6d1736d4 100644 --- a/src/modules/admin/mentor/mentor.controller.ts +++ b/src/modules/admin/mentor/mentor.controller.ts @@ -13,13 +13,14 @@ import { import { MentorService } from './mentor.service'; import { Roles } from 'src/modules/auth/auth.decorator'; import { AuthGuard } from 'src/modules/auth/guard/auth/auth.guard'; +import { RoleGuard } from 'src/modules/auth/guard/role/role.guard'; import { GetMentorDto } from './dto/get-mentor.dto'; import { CreateMentorDto } from './dto/create-mentor.dto'; import { UpdateMentorDto } from './dto/update-mentor.dto'; @Controller('admin/mentor') -@UseGuards(AuthGuard) -@Roles('OWNER') +@UseGuards(AuthGuard, RoleGuard) +@Roles('OWNER', 'ADMIN') export class MentorController { constructor(private readonly mentorService: MentorService) {} From 43b9f7d9082612db58396f0828d31f574f165137 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Thu, 27 Mar 2025 18:00:50 +0300 Subject: [PATCH 06/43] add isActive field to User model with default value true --- prisma/schema.prisma | 1 + 1 file changed, 1 insertion(+) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 769b997a..0f6c64b9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -47,6 +47,7 @@ model User { email String @unique password String imageUrl String? + isActive Boolean @default(true) deletedAt DateTime? createdAt DateTime @default(now()) From 5366a8fed6513160de9c3344bdcd0921ff0e02d9 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Fri, 28 Mar 2025 00:35:24 +0300 Subject: [PATCH 07/43] feat: add AdminProfile module with controller, service, and DTOs for managing admin profiles --- src/app.module.ts | 2 + .../admin-profile.controller.spec.ts | 20 +++ .../adminProfile/admin-profile.controller.ts | 45 +++++++ .../adminProfile/admin-profile.module.ts | 11 ++ .../admin-profile.service.spec.ts | 18 +++ .../adminProfile/admin-profile.service.ts | 117 ++++++++++++++++++ src/modules/adminProfile/dto/admin.dto.ts | 35 ++++++ .../dto/update-admin-profile.dto.ts | 15 +++ .../entities/admin-profile.entity.ts | 1 + 9 files changed, 264 insertions(+) create mode 100644 src/modules/adminProfile/admin-profile.controller.spec.ts create mode 100644 src/modules/adminProfile/admin-profile.controller.ts create mode 100644 src/modules/adminProfile/admin-profile.module.ts create mode 100644 src/modules/adminProfile/admin-profile.service.spec.ts create mode 100644 src/modules/adminProfile/admin-profile.service.ts create mode 100644 src/modules/adminProfile/dto/admin.dto.ts create mode 100644 src/modules/adminProfile/dto/update-admin-profile.dto.ts create mode 100644 src/modules/adminProfile/entities/admin-profile.entity.ts diff --git a/src/app.module.ts b/src/app.module.ts index f4ed7a4a..1321daa6 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,6 +8,7 @@ import { MentorModule } from './modules/mentor/mentor.module'; import { ChatModule } from './modules/chat/chat.module'; import { RabbitmqModule } from './common/rabbitmq/rabbitmq.module'; import { MessageModule } from './modules/message/message.module'; +import { AdminProfileModule } from './modules/adminProfile/admin-profile.module'; @Module({ imports: [ @@ -25,6 +26,7 @@ import { MessageModule } from './modules/message/message.module'; ChatModule, RabbitmqModule, MessageModule, + AdminProfileModule, ], providers: [PrismaService], controllers: [], diff --git a/src/modules/adminProfile/admin-profile.controller.spec.ts b/src/modules/adminProfile/admin-profile.controller.spec.ts new file mode 100644 index 00000000..116acd12 --- /dev/null +++ b/src/modules/adminProfile/admin-profile.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AdminProfileController } from './admin-profile.controller'; +import { AdminProfileService } from './admin-profile.service'; + +describe('AdminProfileController', () => { + let controller: AdminProfileController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AdminProfileController], + providers: [AdminProfileService], + }).compile(); + + controller = module.get(AdminProfileController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/modules/adminProfile/admin-profile.controller.ts b/src/modules/adminProfile/admin-profile.controller.ts new file mode 100644 index 00000000..307a1fc9 --- /dev/null +++ b/src/modules/adminProfile/admin-profile.controller.ts @@ -0,0 +1,45 @@ +import { Controller, Get, Body, Patch, Param, UseGuards, } from '@nestjs/common'; +import { AuthGuard } from 'src/modules/auth/guard/auth/auth.guard'; +import { AdminProfileService } from './admin-profile.service'; +import { UpdateAdminProfileDto } from './dto/update-admin-profile.dto'; +import { RoleGuard } from 'src/modules/auth/guard/role/role.guard'; +import { Roles } from '../auth/auth.decorator'; + +@Controller('admin/profile') +@UseGuards(AuthGuard) +export class AdminProfileController { + constructor(private readonly adminProfileService: AdminProfileService) {} + + @Get(':id') + @UseGuards(RoleGuard) + @Roles('OWNER','ADMIN') + findOne( + @Param('id') id){ + return this.adminProfileService.findOne(id); +} + + + @Patch(':id') + @UseGuards(RoleGuard) + @Roles('OWNER','ADMIN') + update(@Param('id') id: string, @Body() updateAdminProfileDto: UpdateAdminProfileDto) { + return this.adminProfileService.update(id, updateAdminProfileDto); + } + + @Get('all/:accountId') + @UseGuards(RoleGuard) + @Roles('OWNER') + findAll( + @Param('accountId') accountId) { + return this.adminProfileService.findAll(accountId); + } + + @Patch(':id/activate') + @UseGuards(RoleGuard) + @Roles('OWNER') + activate(@Param('id') id: string) { + return this.adminProfileService.toggleActiveStatus(id); + } + +} + diff --git a/src/modules/adminProfile/admin-profile.module.ts b/src/modules/adminProfile/admin-profile.module.ts new file mode 100644 index 00000000..03975374 --- /dev/null +++ b/src/modules/adminProfile/admin-profile.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { AdminProfileService } from './admin-profile.service'; +import { AdminProfileController } from './admin-profile.controller'; +import { PrismaService } from '../prisma/prisma.service'; +import { JwtService } from '@nestjs/jwt'; + +@Module({ + controllers: [AdminProfileController], + providers: [AdminProfileService, PrismaService, JwtService], +}) +export class AdminProfileModule {} diff --git a/src/modules/adminProfile/admin-profile.service.spec.ts b/src/modules/adminProfile/admin-profile.service.spec.ts new file mode 100644 index 00000000..2c85e333 --- /dev/null +++ b/src/modules/adminProfile/admin-profile.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AdminProfileService } from './admin-profile.service'; + +describe('AdminProfileService', () => { + let service: AdminProfileService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AdminProfileService], + }).compile(); + + service = module.get(AdminProfileService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/modules/adminProfile/admin-profile.service.ts b/src/modules/adminProfile/admin-profile.service.ts new file mode 100644 index 00000000..6c3b6cf4 --- /dev/null +++ b/src/modules/adminProfile/admin-profile.service.ts @@ -0,0 +1,117 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from 'src/modules/prisma/prisma.service'; +import { AdminDto } from './dto/admin.dto'; +import { UpdateAdminProfileDto } from './dto/update-admin-profile.dto'; + +@Injectable() +export class AdminProfileService { + constructor( + private prisma: PrismaService, + ) { } + + async findOne(id: string): Promise { + + const admin = await this.prisma.user.findFirst({ + where: { + id: id, + deletedAt: null, + } + }); + + if (!admin) { + throw new NotFoundException('Admin not found'); + } + + return new AdminDto({ ...admin }); + } + + async update(id: string, updateAdminProfileDto: UpdateAdminProfileDto): Promise { + console.log('Updating admin with ID:', id); + + const existingAdmin = await this.prisma.user.findFirst({ + where: { + id: id, + deletedAt: null, + }, + }); + + if (!existingAdmin) { + throw new NotFoundException('Admin not found'); + } + + const updatedAdmin = await this.prisma.user.update({ + where: { id: id }, + data: { + ...updateAdminProfileDto, + }, + }); + + console.log('Updated admin:', updatedAdmin); + + return new AdminDto({ ...updatedAdmin }); + } + + async findAll(accountId: string): Promise { + console.log('accountId:', accountId); + + const admins = await this.prisma.user.findMany({ + where: { + deletedAt: null, + AccountUser: { + some: { + accountId: accountId, + Role: { + name: 'Admin', + }, + }, + }, + }, + include: { + AccountUser: { + include: { + Role: true, + }, + }, + }, + }); + + console.log('Filtered Admin Data:', JSON.stringify(admins, null, 2)); + + return admins.map(admin => { + + return new AdminDto({ + id: admin.id, + name: admin.name, + email: admin.email, + isActive: admin.isActive + }); + }); + } + + async toggleActiveStatus(id: string): Promise { + + const existingAdmin = await this.prisma.user.findFirst({ + where: { + id: id, + deletedAt: null, + }, + }); + + if (!existingAdmin) { + throw new NotFoundException('Admin not found'); + } + + const updatedAdmin = await this.prisma.user.update({ + where: { id: id }, + data: { + isActive: !existingAdmin.isActive, + }, + }); + + return new AdminDto({ ...updatedAdmin }); + + + + } + +} diff --git a/src/modules/adminProfile/dto/admin.dto.ts b/src/modules/adminProfile/dto/admin.dto.ts new file mode 100644 index 00000000..b9752891 --- /dev/null +++ b/src/modules/adminProfile/dto/admin.dto.ts @@ -0,0 +1,35 @@ +// src/posts/dto/post.dto.ts + +import { Expose } from 'class-transformer'; + +export class AdminDto { + @Expose() + id: string; + + @Expose() + name: string; + + @Expose() + email: string; + + @Expose() + isActive: boolean; + + @Expose() + AccountUser: any; + + @Expose() + roleName: string; // Add the roleName field + + constructor(partial: Partial) { + // console.log('partial', partial); + Object.assign(this, { + id: partial.id, + name: partial.name, + email: partial.email, + isActive: partial.isActive, + AccountUser: partial.AccountUser + }); + } +} + diff --git a/src/modules/adminProfile/dto/update-admin-profile.dto.ts b/src/modules/adminProfile/dto/update-admin-profile.dto.ts new file mode 100644 index 00000000..6fd0f9dc --- /dev/null +++ b/src/modules/adminProfile/dto/update-admin-profile.dto.ts @@ -0,0 +1,15 @@ +import { IsOptional, IsString, IsBoolean } from 'class-validator'; + +export class UpdateAdminProfileDto { + @IsOptional() + @IsString() + name?: string; + + @IsOptional() + @IsString() + email?: string; + + @IsOptional() + @IsString() + password?: string; +} \ No newline at end of file diff --git a/src/modules/adminProfile/entities/admin-profile.entity.ts b/src/modules/adminProfile/entities/admin-profile.entity.ts new file mode 100644 index 00000000..fb474a13 --- /dev/null +++ b/src/modules/adminProfile/entities/admin-profile.entity.ts @@ -0,0 +1 @@ +export class AdminProfile {} From 418a3d50eab8a41a7083e00711f0d4b27f3d06b2 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Fri, 28 Mar 2025 00:39:15 +0300 Subject: [PATCH 08/43] refactor: clean up code formatting and improve readability in AdminProfile module --- .../adminProfile/admin-profile.controller.ts | 24 ++++++++-------- .../adminProfile/admin-profile.service.ts | 28 ++++++++----------- src/modules/adminProfile/dto/admin.dto.ts | 5 ++-- .../dto/update-admin-profile.dto.ts | 4 +-- 4 files changed, 26 insertions(+), 35 deletions(-) diff --git a/src/modules/adminProfile/admin-profile.controller.ts b/src/modules/adminProfile/admin-profile.controller.ts index 307a1fc9..8eb8e1af 100644 --- a/src/modules/adminProfile/admin-profile.controller.ts +++ b/src/modules/adminProfile/admin-profile.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Body, Patch, Param, UseGuards, } from '@nestjs/common'; +import { Controller, Get, Body, Patch, Param, UseGuards } from '@nestjs/common'; import { AuthGuard } from 'src/modules/auth/guard/auth/auth.guard'; import { AdminProfileService } from './admin-profile.service'; import { UpdateAdminProfileDto } from './dto/update-admin-profile.dto'; @@ -12,25 +12,25 @@ export class AdminProfileController { @Get(':id') @UseGuards(RoleGuard) - @Roles('OWNER','ADMIN') - findOne( - @Param('id') id){ - return this.adminProfileService.findOne(id); -} - + @Roles('OWNER', 'ADMIN') + findOne(@Param('id') id) { + return this.adminProfileService.findOne(id); + } @Patch(':id') @UseGuards(RoleGuard) - @Roles('OWNER','ADMIN') - update(@Param('id') id: string, @Body() updateAdminProfileDto: UpdateAdminProfileDto) { + @Roles('OWNER', 'ADMIN') + update( + @Param('id') id: string, + @Body() updateAdminProfileDto: UpdateAdminProfileDto, + ) { return this.adminProfileService.update(id, updateAdminProfileDto); } @Get('all/:accountId') @UseGuards(RoleGuard) @Roles('OWNER') - findAll( - @Param('accountId') accountId) { + findAll(@Param('accountId') accountId) { return this.adminProfileService.findAll(accountId); } @@ -40,6 +40,4 @@ export class AdminProfileController { activate(@Param('id') id: string) { return this.adminProfileService.toggleActiveStatus(id); } - } - diff --git a/src/modules/adminProfile/admin-profile.service.ts b/src/modules/adminProfile/admin-profile.service.ts index 6c3b6cf4..0893d595 100644 --- a/src/modules/adminProfile/admin-profile.service.ts +++ b/src/modules/adminProfile/admin-profile.service.ts @@ -5,17 +5,14 @@ import { UpdateAdminProfileDto } from './dto/update-admin-profile.dto'; @Injectable() export class AdminProfileService { - constructor( - private prisma: PrismaService, - ) { } - - async findOne(id: string): Promise { + constructor(private prisma: PrismaService) {} + async findOne(id: string): Promise { const admin = await this.prisma.user.findFirst({ where: { id: id, deletedAt: null, - } + }, }); if (!admin) { @@ -25,7 +22,10 @@ export class AdminProfileService { return new AdminDto({ ...admin }); } - async update(id: string, updateAdminProfileDto: UpdateAdminProfileDto): Promise { + async update( + id: string, + updateAdminProfileDto: UpdateAdminProfileDto, + ): Promise { console.log('Updating admin with ID:', id); const existingAdmin = await this.prisma.user.findFirst({ @@ -59,7 +59,7 @@ export class AdminProfileService { deletedAt: null, AccountUser: { some: { - accountId: accountId, + accountId: accountId, Role: { name: 'Admin', }, @@ -77,19 +77,17 @@ export class AdminProfileService { console.log('Filtered Admin Data:', JSON.stringify(admins, null, 2)); - return admins.map(admin => { - + return admins.map((admin) => { return new AdminDto({ id: admin.id, name: admin.name, email: admin.email, - isActive: admin.isActive + isActive: admin.isActive, }); }); } - - async toggleActiveStatus(id: string): Promise { + async toggleActiveStatus(id: string): Promise { const existingAdmin = await this.prisma.user.findFirst({ where: { id: id, @@ -109,9 +107,5 @@ export class AdminProfileService { }); return new AdminDto({ ...updatedAdmin }); - - - } - } diff --git a/src/modules/adminProfile/dto/admin.dto.ts b/src/modules/adminProfile/dto/admin.dto.ts index b9752891..84f2851a 100644 --- a/src/modules/adminProfile/dto/admin.dto.ts +++ b/src/modules/adminProfile/dto/admin.dto.ts @@ -22,14 +22,13 @@ export class AdminDto { roleName: string; // Add the roleName field constructor(partial: Partial) { - // console.log('partial', partial); + // console.log('partial', partial); Object.assign(this, { id: partial.id, name: partial.name, email: partial.email, isActive: partial.isActive, - AccountUser: partial.AccountUser + AccountUser: partial.AccountUser, }); } } - diff --git a/src/modules/adminProfile/dto/update-admin-profile.dto.ts b/src/modules/adminProfile/dto/update-admin-profile.dto.ts index 6fd0f9dc..322191ed 100644 --- a/src/modules/adminProfile/dto/update-admin-profile.dto.ts +++ b/src/modules/adminProfile/dto/update-admin-profile.dto.ts @@ -1,4 +1,4 @@ -import { IsOptional, IsString, IsBoolean } from 'class-validator'; +import { IsOptional, IsString } from 'class-validator'; export class UpdateAdminProfileDto { @IsOptional() @@ -12,4 +12,4 @@ export class UpdateAdminProfileDto { @IsOptional() @IsString() password?: string; -} \ No newline at end of file +} From 53f3ae40c5fc7ce6d796f44c94f34f5c9583fe5a Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Fri, 28 Mar 2025 13:58:30 +0300 Subject: [PATCH 09/43] fix: improve role guard logic to support multiple user accounts --- src/modules/auth/guard/role/role.guard.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/modules/auth/guard/role/role.guard.ts b/src/modules/auth/guard/role/role.guard.ts index b9744b2c..08bbc6e2 100644 --- a/src/modules/auth/guard/role/role.guard.ts +++ b/src/modules/auth/guard/role/role.guard.ts @@ -16,9 +16,10 @@ export class RoleGuard implements CanActivate { const user = context.switchToHttp().getRequest().user; - return requiredRoles.some( - (role) => - user.accounts?.[0]?.role?.name.toLowerCase() === role.toLowerCase(), + return user.accounts?.some((account) => + requiredRoles.some( + (role) => account.role?.name.toLowerCase() === role.toLowerCase(), + ), ); } } From 4bafee3fea2ddf3b579bde247beb24bf5943c8b7 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Fri, 28 Mar 2025 15:53:25 +0300 Subject: [PATCH 10/43] feat: enhance admin profile management with account-based user activation and role checks --- prisma/schema.prisma | 2 +- .../adminProfile/admin-profile.controller.ts | 18 ++- .../adminProfile/admin-profile.service.ts | 104 ++++++++++++++---- src/modules/auth/guard/role/role.guard.ts | 34 ++++-- 4 files changed, 118 insertions(+), 40 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0f6c64b9..82c72379 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -47,7 +47,6 @@ model User { email String @unique password String imageUrl String? - isActive Boolean @default(true) deletedAt DateTime? createdAt DateTime @default(now()) @@ -61,6 +60,7 @@ model AccountUser { userId String roleId String accountId String + isActive Boolean @default(true) deletedAt DateTime? createdAt DateTime @default(now()) diff --git a/src/modules/adminProfile/admin-profile.controller.ts b/src/modules/adminProfile/admin-profile.controller.ts index 8eb8e1af..4039a5ca 100644 --- a/src/modules/adminProfile/admin-profile.controller.ts +++ b/src/modules/adminProfile/admin-profile.controller.ts @@ -10,11 +10,14 @@ import { Roles } from '../auth/auth.decorator'; export class AdminProfileController { constructor(private readonly adminProfileService: AdminProfileService) {} - @Get(':id') + @Get(':accountId/:userId/') @UseGuards(RoleGuard) @Roles('OWNER', 'ADMIN') - findOne(@Param('id') id) { - return this.adminProfileService.findOne(id); + findOne( + @Param('userId') userId: string, + @Param('accountId') accountId: string, + ) { + return this.adminProfileService.findOne(userId, accountId); } @Patch(':id') @@ -34,10 +37,13 @@ export class AdminProfileController { return this.adminProfileService.findAll(accountId); } - @Patch(':id/activate') + @Patch(':userId/activate/:accountId') @UseGuards(RoleGuard) @Roles('OWNER') - activate(@Param('id') id: string) { - return this.adminProfileService.toggleActiveStatus(id); + activate( + @Param('userId') userId: string, + @Param('accountId') accountId: string, + ) { + return this.adminProfileService.toggleActiveStatus(userId, accountId); } } diff --git a/src/modules/adminProfile/admin-profile.service.ts b/src/modules/adminProfile/admin-profile.service.ts index 0893d595..39a9dbe3 100644 --- a/src/modules/adminProfile/admin-profile.service.ts +++ b/src/modules/adminProfile/admin-profile.service.ts @@ -7,19 +7,35 @@ import { UpdateAdminProfileDto } from './dto/update-admin-profile.dto'; export class AdminProfileService { constructor(private prisma: PrismaService) {} - async findOne(id: string): Promise { + async findOne(userId: string, accountId: string): Promise { const admin = await this.prisma.user.findFirst({ where: { - id: id, + id: userId, deletedAt: null, }, + include: { + AccountUser: { + where: { + accountId: accountId, + }, + }, + }, }); if (!admin) { throw new NotFoundException('Admin not found'); } - return new AdminDto({ ...admin }); + const accountUser = admin.AccountUser.find( + (au) => au.accountId === accountId, + ); + + return new AdminDto({ + id: admin.id, + name: admin.name, + email: admin.email, + isActive: accountUser?.isActive, + }); } async update( @@ -52,19 +68,31 @@ export class AdminProfileService { } async findAll(accountId: string): Promise { - console.log('accountId:', accountId); - const admins = await this.prisma.user.findMany({ where: { deletedAt: null, - AccountUser: { - some: { - accountId: accountId, - Role: { - name: 'Admin', + OR: [ + { + AccountUser: { + some: { + accountId: accountId, + Role: { + name: 'Admin', + }, + }, }, }, - }, + { + AccountUser: { + some: { + accountId: accountId, + Role: { + name: 'Owner', + }, + }, + }, + }, + ], }, include: { AccountUser: { @@ -78,34 +106,62 @@ export class AdminProfileService { console.log('Filtered Admin Data:', JSON.stringify(admins, null, 2)); return admins.map((admin) => { + const accountUser = admin.AccountUser.find( + (au) => au.accountId === accountId, + ); + return new AdminDto({ id: admin.id, name: admin.name, email: admin.email, - isActive: admin.isActive, + isActive: accountUser?.isActive, }); }); } - async toggleActiveStatus(id: string): Promise { - const existingAdmin = await this.prisma.user.findFirst({ + async toggleActiveStatus( + accountId: string, + userId: string, + ): Promise { + const accountUser = await this.prisma.accountUser.findFirst({ where: { - id: id, - deletedAt: null, + accountId: accountId, + userId: userId, }, }); - if (!existingAdmin) { - throw new NotFoundException('Admin not found'); - } - - const updatedAdmin = await this.prisma.user.update({ - where: { id: id }, + const updatedAccountUser = await this.prisma.accountUser.update({ + where: { + id: accountUser.id, + }, data: { - isActive: !existingAdmin.isActive, + isActive: !accountUser.isActive, }, }); - return new AdminDto({ ...updatedAdmin }); + const updatedUser = await this.prisma.user.findFirst({ + where: { + id: userId, + deletedAt: null, + }, + include: { + AccountUser: { + where: { + accountId: accountId, + }, + }, + }, + }); + + if (!updatedUser) { + throw new NotFoundException('User not found'); + } + + return new AdminDto({ + id: updatedUser.id, + name: updatedUser.name, + email: updatedUser.email, + isActive: updatedAccountUser.isActive, + }); } } diff --git a/src/modules/auth/guard/role/role.guard.ts b/src/modules/auth/guard/role/role.guard.ts index 08bbc6e2..c676ac70 100644 --- a/src/modules/auth/guard/role/role.guard.ts +++ b/src/modules/auth/guard/role/role.guard.ts @@ -1,14 +1,16 @@ import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; -import { Observable } from 'rxjs'; import { ROLE_KEY } from '../../auth.decorator'; +import { PrismaService } from 'src/modules/prisma/prisma.service'; @Injectable() export class RoleGuard implements CanActivate { - constructor(private reflector: Reflector) {} - canActivate( - context: ExecutionContext, - ): boolean | Promise | Observable { + constructor( + private reflector: Reflector, + private prisma: PrismaService, + ) {} + + async canActivate(context: ExecutionContext): Promise { const requiredRoles = this.reflector.getAllAndOverride(ROLE_KEY, [ context.getHandler(), context.getClass(), @@ -16,10 +18,24 @@ export class RoleGuard implements CanActivate { const user = context.switchToHttp().getRequest().user; - return user.accounts?.some((account) => - requiredRoles.some( - (role) => account.role?.name.toLowerCase() === role.toLowerCase(), - ), + const activeAccounts = await this.prisma.accountUser.findMany({ + where: { + userId: user.sub, + isActive: true, + }, + select: { + accountId: true, + }, + }); + + const activeAccountIds = activeAccounts.map((account) => account.accountId); + + return user.accounts.some( + (account) => + activeAccountIds.includes(account.id) && + requiredRoles.some( + (role) => account.role?.name.toLowerCase() === role.toLowerCase(), + ), ); } } From 0cf541cd6c73d876e303222ab7a8e9dfd62ea724 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Fri, 28 Mar 2025 16:32:13 +0300 Subject: [PATCH 11/43] feat: update User entity to include optional fields and enhance role guard with user type --- src/modules/admin/user/entities/user.entity.ts | 4 +++- src/modules/auth/guard/role/role.guard.ts | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/modules/admin/user/entities/user.entity.ts b/src/modules/admin/user/entities/user.entity.ts index b014e0e7..9560fa74 100644 --- a/src/modules/admin/user/entities/user.entity.ts +++ b/src/modules/admin/user/entities/user.entity.ts @@ -1,5 +1,7 @@ export class User { - id: string; + id?: string; + sub?: string; name: string; email: string; + accounts: any } diff --git a/src/modules/auth/guard/role/role.guard.ts b/src/modules/auth/guard/role/role.guard.ts index c676ac70..820aa7e3 100644 --- a/src/modules/auth/guard/role/role.guard.ts +++ b/src/modules/auth/guard/role/role.guard.ts @@ -2,6 +2,7 @@ import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { ROLE_KEY } from '../../auth.decorator'; import { PrismaService } from 'src/modules/prisma/prisma.service'; +import { User } from 'src/modules/admin/user/entities/user.entity'; @Injectable() export class RoleGuard implements CanActivate { @@ -16,7 +17,9 @@ export class RoleGuard implements CanActivate { context.getClass(), ]); - const user = context.switchToHttp().getRequest().user; + const user: User = context.switchToHttp().getRequest().user; + + console.log('User:', user); const activeAccounts = await this.prisma.accountUser.findMany({ where: { From ce941538f889d8b576930dc228707323edaaeb29 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Fri, 28 Mar 2025 16:33:06 +0300 Subject: [PATCH 12/43] fix: remove debug logging from RoleGuard to clean up code --- src/modules/auth/guard/role/role.guard.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/modules/auth/guard/role/role.guard.ts b/src/modules/auth/guard/role/role.guard.ts index 820aa7e3..3018b606 100644 --- a/src/modules/auth/guard/role/role.guard.ts +++ b/src/modules/auth/guard/role/role.guard.ts @@ -19,8 +19,6 @@ export class RoleGuard implements CanActivate { const user: User = context.switchToHttp().getRequest().user; - console.log('User:', user); - const activeAccounts = await this.prisma.accountUser.findMany({ where: { userId: user.sub, From bd8fba2a683cc6b056abac432a75ea5901d0a622 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Fri, 28 Mar 2025 16:35:08 +0300 Subject: [PATCH 13/43] fix: change User entity id field from optional to required --- src/modules/admin/user/entities/user.entity.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/admin/user/entities/user.entity.ts b/src/modules/admin/user/entities/user.entity.ts index 9560fa74..aaa3099d 100644 --- a/src/modules/admin/user/entities/user.entity.ts +++ b/src/modules/admin/user/entities/user.entity.ts @@ -1,7 +1,7 @@ export class User { - id?: string; + id: string; sub?: string; name: string; email: string; - accounts: any + accounts: any; } From 918715bcb8b312e93fcd7df8f841ec98d88c3c1b Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Sat, 29 Mar 2025 01:29:09 +0300 Subject: [PATCH 14/43] fix: update User entity to remove optional sub field and adjust AuthGuard and RoleGuard to use id instead --- src/modules/admin/user/entities/user.entity.ts | 1 - src/modules/auth/guard/auth/auth.guard.ts | 6 +++++- src/modules/auth/guard/role/role.guard.ts | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/modules/admin/user/entities/user.entity.ts b/src/modules/admin/user/entities/user.entity.ts index aaa3099d..d86a6ea4 100644 --- a/src/modules/admin/user/entities/user.entity.ts +++ b/src/modules/admin/user/entities/user.entity.ts @@ -1,6 +1,5 @@ export class User { id: string; - sub?: string; name: string; email: string; accounts: any; diff --git a/src/modules/auth/guard/auth/auth.guard.ts b/src/modules/auth/guard/auth/auth.guard.ts index bb905c49..7f917f91 100644 --- a/src/modules/auth/guard/auth/auth.guard.ts +++ b/src/modules/auth/guard/auth/auth.guard.ts @@ -31,7 +31,11 @@ export class AuthGuard implements CanActivate { const user = await this.jwtService.verifyAsync(token, options); - request.user = user; + request.user = { + ...user, + id: user.sub, + }; + return true; } catch (error) { console.error(`AuthGuard Error: ${error.message}`); diff --git a/src/modules/auth/guard/role/role.guard.ts b/src/modules/auth/guard/role/role.guard.ts index 3018b606..f63497b8 100644 --- a/src/modules/auth/guard/role/role.guard.ts +++ b/src/modules/auth/guard/role/role.guard.ts @@ -19,9 +19,11 @@ export class RoleGuard implements CanActivate { const user: User = context.switchToHttp().getRequest().user; + console.log('User from role guard', user); + const activeAccounts = await this.prisma.accountUser.findMany({ where: { - userId: user.sub, + userId: user.id, isActive: true, }, select: { From b5f4937dbbba0fe2f3d91ec637ef5f02255b9132 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Sat, 29 Mar 2025 01:37:33 +0300 Subject: [PATCH 15/43] feat: enhance RoleGuard to throw exceptions for inactive accounts and missing roles --- src/modules/auth/guard/role/role.guard.ts | 26 +++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/modules/auth/guard/role/role.guard.ts b/src/modules/auth/guard/role/role.guard.ts index f63497b8..58814000 100644 --- a/src/modules/auth/guard/role/role.guard.ts +++ b/src/modules/auth/guard/role/role.guard.ts @@ -1,4 +1,10 @@ -import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { + CanActivate, + ExecutionContext, + Injectable, + ForbiddenException, + NotFoundException, +} from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { ROLE_KEY } from '../../auth.decorator'; import { PrismaService } from 'src/modules/prisma/prisma.service'; @@ -33,12 +39,28 @@ export class RoleGuard implements CanActivate { const activeAccountIds = activeAccounts.map((account) => account.accountId); - return user.accounts.some( + const hasActiveAccount = user.accounts.some((account) => + activeAccountIds.includes(account.id), + ); + + if (!hasActiveAccount) { + throw new ForbiddenException( + "Your organization's owner has disabled your account", + ); + } + + const hasRequiredRole = user.accounts.some( (account) => activeAccountIds.includes(account.id) && requiredRoles.some( (role) => account.role?.name.toLowerCase() === role.toLowerCase(), ), ); + + if (!hasRequiredRole) { + throw new NotFoundException('Forbidden resource'); + } + + return true; } } From 90bccfe6bbdfe5177bbc647017a8a04c460e9417 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Sat, 29 Mar 2025 01:54:43 +0300 Subject: [PATCH 16/43] fix: remove debug logging from RoleGuard to improve code clarity --- src/modules/auth/guard/role/role.guard.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/modules/auth/guard/role/role.guard.ts b/src/modules/auth/guard/role/role.guard.ts index 58814000..3cf8b57f 100644 --- a/src/modules/auth/guard/role/role.guard.ts +++ b/src/modules/auth/guard/role/role.guard.ts @@ -25,8 +25,6 @@ export class RoleGuard implements CanActivate { const user: User = context.switchToHttp().getRequest().user; - console.log('User from role guard', user); - const activeAccounts = await this.prisma.accountUser.findMany({ where: { userId: user.id, From 3805a2247ca5d58e7a814fe8d929b5b2c99f6270 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Sat, 29 Mar 2025 21:22:14 +0300 Subject: [PATCH 17/43] fix: update route parameter for findAll method in AdminProfileController --- src/modules/adminProfile/admin-profile.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/adminProfile/admin-profile.controller.ts b/src/modules/adminProfile/admin-profile.controller.ts index 4039a5ca..7ba2c2a0 100644 --- a/src/modules/adminProfile/admin-profile.controller.ts +++ b/src/modules/adminProfile/admin-profile.controller.ts @@ -30,7 +30,7 @@ export class AdminProfileController { return this.adminProfileService.update(id, updateAdminProfileDto); } - @Get('all/:accountId') + @Get(':accountId') @UseGuards(RoleGuard) @Roles('OWNER') findAll(@Param('accountId') accountId) { From faa2e05cf232c87e9097203252d44b6b91ea941c Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Sat, 29 Mar 2025 22:02:05 +0300 Subject: [PATCH 18/43] feat: add Role module with controller, service, and DTOs for role management --- src/app.module.ts | 2 + src/modules/admin/role/dto/create-role.dto.ts | 1 + src/modules/admin/role/dto/update-role.dto.ts | 4 ++ .../admin/role/entities/role.entity.ts | 1 + .../admin/role/role.controller.spec.ts | 20 ++++++++++ src/modules/admin/role/role.controller.ts | 18 +++++++++ src/modules/admin/role/role.module.ts | 11 ++++++ src/modules/admin/role/role.service.spec.ts | 18 +++++++++ src/modules/admin/role/role.service.ts | 39 +++++++++++++++++++ 9 files changed, 114 insertions(+) create mode 100644 src/modules/admin/role/dto/create-role.dto.ts create mode 100644 src/modules/admin/role/dto/update-role.dto.ts create mode 100644 src/modules/admin/role/entities/role.entity.ts create mode 100644 src/modules/admin/role/role.controller.spec.ts create mode 100644 src/modules/admin/role/role.controller.ts create mode 100644 src/modules/admin/role/role.module.ts create mode 100644 src/modules/admin/role/role.service.spec.ts create mode 100644 src/modules/admin/role/role.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index 1321daa6..98d41047 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -9,6 +9,7 @@ import { ChatModule } from './modules/chat/chat.module'; import { RabbitmqModule } from './common/rabbitmq/rabbitmq.module'; import { MessageModule } from './modules/message/message.module'; import { AdminProfileModule } from './modules/adminProfile/admin-profile.module'; +import { RoleModule } from './modules/admin/role/role.module'; @Module({ imports: [ @@ -27,6 +28,7 @@ import { AdminProfileModule } from './modules/adminProfile/admin-profile.module' RabbitmqModule, MessageModule, AdminProfileModule, + RoleModule, ], providers: [PrismaService], controllers: [], diff --git a/src/modules/admin/role/dto/create-role.dto.ts b/src/modules/admin/role/dto/create-role.dto.ts new file mode 100644 index 00000000..3044c2b0 --- /dev/null +++ b/src/modules/admin/role/dto/create-role.dto.ts @@ -0,0 +1 @@ +export class CreateRoleDto {} diff --git a/src/modules/admin/role/dto/update-role.dto.ts b/src/modules/admin/role/dto/update-role.dto.ts new file mode 100644 index 00000000..10e9f33c --- /dev/null +++ b/src/modules/admin/role/dto/update-role.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateRoleDto } from './create-role.dto'; + +export class UpdateRoleDto extends PartialType(CreateRoleDto) {} diff --git a/src/modules/admin/role/entities/role.entity.ts b/src/modules/admin/role/entities/role.entity.ts new file mode 100644 index 00000000..ec816d51 --- /dev/null +++ b/src/modules/admin/role/entities/role.entity.ts @@ -0,0 +1 @@ +export class Role {} diff --git a/src/modules/admin/role/role.controller.spec.ts b/src/modules/admin/role/role.controller.spec.ts new file mode 100644 index 00000000..7853f98b --- /dev/null +++ b/src/modules/admin/role/role.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RoleController } from './role.controller'; +import { RoleService } from './role.service'; + +describe('RoleController', () => { + let controller: RoleController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [RoleController], + providers: [RoleService], + }).compile(); + + controller = module.get(RoleController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/modules/admin/role/role.controller.ts b/src/modules/admin/role/role.controller.ts new file mode 100644 index 00000000..4e5f78a0 --- /dev/null +++ b/src/modules/admin/role/role.controller.ts @@ -0,0 +1,18 @@ +import { Controller, Get, Param, UseGuards } from '@nestjs/common'; +import { RoleGuard } from '../../auth/guard/role/role.guard'; +import { Roles } from '../../auth/auth.decorator'; +import { RoleService } from './role.service'; +import { AuthGuard } from 'src/modules/auth/guard/auth/auth.guard'; + +@Controller('roles') +@UseGuards(AuthGuard) +export class RolesController { + constructor(private readonly roleService: RoleService) {} + + @Get(':accountId') + @UseGuards(RoleGuard) + @Roles('OWNER', 'ADMIN') + findAll(@Param('accountId') accountId) { + return this.roleService.findAll(accountId); + } +} diff --git a/src/modules/admin/role/role.module.ts b/src/modules/admin/role/role.module.ts new file mode 100644 index 00000000..6bcf13e2 --- /dev/null +++ b/src/modules/admin/role/role.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { RoleService } from './role.service'; +import { RolesController } from './role.controller'; +import { PrismaService } from 'src/modules/prisma/prisma.service'; +import { JwtService } from '@nestjs/jwt'; + +@Module({ + controllers: [RolesController], + providers: [RoleService, PrismaService, JwtService], +}) +export class RoleModule {} diff --git a/src/modules/admin/role/role.service.spec.ts b/src/modules/admin/role/role.service.spec.ts new file mode 100644 index 00000000..e1e0c008 --- /dev/null +++ b/src/modules/admin/role/role.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RoleService } from './role.service'; + +describe('RoleService', () => { + let service: RoleService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [RoleService], + }).compile(); + + service = module.get(RoleService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/modules/admin/role/role.service.ts b/src/modules/admin/role/role.service.ts new file mode 100644 index 00000000..d51dfbc9 --- /dev/null +++ b/src/modules/admin/role/role.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@nestjs/common'; +//import { CreateRoleDto } from './dto/create-role.dto'; +//import { UpdateRoleDto } from './dto/update-role.dto'; +import { PrismaService } from '../../prisma/prisma.service'; + +@Injectable() +export class RoleService { + constructor(private readonly prisma: PrismaService) {} + /* create(createRoleDto: CreateRoleDto) { + return 'This action adds a new role'; + } */ + + findAll(accountId: string) { + return this.prisma.role.findMany({ + where: { + accountId: accountId, + }, + select: { + id: true, + name: true, + accountId: true, + createdAt: true, + updatedAt: true, + }, + }); + } + + findOne(id: number) { + return `This action returns a #${id} role`; + } + + /* update(id: number, updateRoleDto: UpdateRoleDto) { + return `This action updates a #${id} role`; + } */ + + remove(id: number) { + return `This action removes a #${id} role`; + } +} From a7dcb2be355eb8cd556c99481129dcd3afcd5aed Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Sat, 29 Mar 2025 22:27:39 +0300 Subject: [PATCH 19/43] feat: implement AdminProfile module with controller, service, DTOs, and tests for admin profile management --- src/app.module.ts | 2 +- .../adminProfile/admin-profile.controller.spec.ts | 0 .../{ => admin}/adminProfile/admin-profile.controller.ts | 6 +++--- .../{ => admin}/adminProfile/admin-profile.module.ts | 2 +- .../{ => admin}/adminProfile/admin-profile.service.spec.ts | 0 .../{ => admin}/adminProfile/admin-profile.service.ts | 0 src/modules/{ => admin}/adminProfile/dto/admin.dto.ts | 0 .../adminProfile/dto/update-admin-profile.dto.ts | 0 .../adminProfile/entities/admin-profile.entity.ts | 0 9 files changed, 5 insertions(+), 5 deletions(-) rename src/modules/{ => admin}/adminProfile/admin-profile.controller.spec.ts (100%) rename src/modules/{ => admin}/adminProfile/admin-profile.controller.ts (91%) rename src/modules/{ => admin}/adminProfile/admin-profile.module.ts (85%) rename src/modules/{ => admin}/adminProfile/admin-profile.service.spec.ts (100%) rename src/modules/{ => admin}/adminProfile/admin-profile.service.ts (100%) rename src/modules/{ => admin}/adminProfile/dto/admin.dto.ts (100%) rename src/modules/{ => admin}/adminProfile/dto/update-admin-profile.dto.ts (100%) rename src/modules/{ => admin}/adminProfile/entities/admin-profile.entity.ts (100%) diff --git a/src/app.module.ts b/src/app.module.ts index 98d41047..e8f2327a 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,7 +8,7 @@ import { MentorModule } from './modules/mentor/mentor.module'; import { ChatModule } from './modules/chat/chat.module'; import { RabbitmqModule } from './common/rabbitmq/rabbitmq.module'; import { MessageModule } from './modules/message/message.module'; -import { AdminProfileModule } from './modules/adminProfile/admin-profile.module'; +import { AdminProfileModule } from './modules/admin/adminProfile/admin-profile.module'; import { RoleModule } from './modules/admin/role/role.module'; @Module({ diff --git a/src/modules/adminProfile/admin-profile.controller.spec.ts b/src/modules/admin/adminProfile/admin-profile.controller.spec.ts similarity index 100% rename from src/modules/adminProfile/admin-profile.controller.spec.ts rename to src/modules/admin/adminProfile/admin-profile.controller.spec.ts diff --git a/src/modules/adminProfile/admin-profile.controller.ts b/src/modules/admin/adminProfile/admin-profile.controller.ts similarity index 91% rename from src/modules/adminProfile/admin-profile.controller.ts rename to src/modules/admin/adminProfile/admin-profile.controller.ts index 7ba2c2a0..f97b57d6 100644 --- a/src/modules/adminProfile/admin-profile.controller.ts +++ b/src/modules/admin/adminProfile/admin-profile.controller.ts @@ -3,7 +3,7 @@ import { AuthGuard } from 'src/modules/auth/guard/auth/auth.guard'; import { AdminProfileService } from './admin-profile.service'; import { UpdateAdminProfileDto } from './dto/update-admin-profile.dto'; import { RoleGuard } from 'src/modules/auth/guard/role/role.guard'; -import { Roles } from '../auth/auth.decorator'; +import { Roles } from '../../auth/auth.decorator'; @Controller('admin/profile') @UseGuards(AuthGuard) @@ -41,9 +41,9 @@ export class AdminProfileController { @UseGuards(RoleGuard) @Roles('OWNER') activate( - @Param('userId') userId: string, @Param('accountId') accountId: string, + @Param('userId') userId: string, ) { - return this.adminProfileService.toggleActiveStatus(userId, accountId); + return this.adminProfileService.toggleActiveStatus(accountId, userId); } } diff --git a/src/modules/adminProfile/admin-profile.module.ts b/src/modules/admin/adminProfile/admin-profile.module.ts similarity index 85% rename from src/modules/adminProfile/admin-profile.module.ts rename to src/modules/admin/adminProfile/admin-profile.module.ts index 03975374..705d96ec 100644 --- a/src/modules/adminProfile/admin-profile.module.ts +++ b/src/modules/admin/adminProfile/admin-profile.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { AdminProfileService } from './admin-profile.service'; import { AdminProfileController } from './admin-profile.controller'; -import { PrismaService } from '../prisma/prisma.service'; +import { PrismaService } from '../../prisma/prisma.service'; import { JwtService } from '@nestjs/jwt'; @Module({ diff --git a/src/modules/adminProfile/admin-profile.service.spec.ts b/src/modules/admin/adminProfile/admin-profile.service.spec.ts similarity index 100% rename from src/modules/adminProfile/admin-profile.service.spec.ts rename to src/modules/admin/adminProfile/admin-profile.service.spec.ts diff --git a/src/modules/adminProfile/admin-profile.service.ts b/src/modules/admin/adminProfile/admin-profile.service.ts similarity index 100% rename from src/modules/adminProfile/admin-profile.service.ts rename to src/modules/admin/adminProfile/admin-profile.service.ts diff --git a/src/modules/adminProfile/dto/admin.dto.ts b/src/modules/admin/adminProfile/dto/admin.dto.ts similarity index 100% rename from src/modules/adminProfile/dto/admin.dto.ts rename to src/modules/admin/adminProfile/dto/admin.dto.ts diff --git a/src/modules/adminProfile/dto/update-admin-profile.dto.ts b/src/modules/admin/adminProfile/dto/update-admin-profile.dto.ts similarity index 100% rename from src/modules/adminProfile/dto/update-admin-profile.dto.ts rename to src/modules/admin/adminProfile/dto/update-admin-profile.dto.ts diff --git a/src/modules/adminProfile/entities/admin-profile.entity.ts b/src/modules/admin/adminProfile/entities/admin-profile.entity.ts similarity index 100% rename from src/modules/adminProfile/entities/admin-profile.entity.ts rename to src/modules/admin/adminProfile/entities/admin-profile.entity.ts From 987fcb81ed7a146d813cde3c2d51c5df6cc6cf6c Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Tue, 1 Apr 2025 09:46:08 +0300 Subject: [PATCH 20/43] changed adminProfile to profile --- src/app.module.ts | 4 +- src/modules/admin/profile/dto/profile.dto.ts | 34 ++++ .../admin/profile/dto/update-profile.dto.ts | 15 ++ .../admin/profile/entities/profile.entity.ts | 1 + .../admin/profile/profile.controller.spec.ts | 20 +++ .../admin/profile/profile.controller.ts | 49 +++++ src/modules/admin/profile/profile.module.ts | 11 ++ .../admin/profile/profile.service.spec.ts | 18 ++ src/modules/admin/profile/profile.service.ts | 167 ++++++++++++++++++ 9 files changed, 317 insertions(+), 2 deletions(-) create mode 100644 src/modules/admin/profile/dto/profile.dto.ts create mode 100644 src/modules/admin/profile/dto/update-profile.dto.ts create mode 100644 src/modules/admin/profile/entities/profile.entity.ts create mode 100644 src/modules/admin/profile/profile.controller.spec.ts create mode 100644 src/modules/admin/profile/profile.controller.ts create mode 100644 src/modules/admin/profile/profile.module.ts create mode 100644 src/modules/admin/profile/profile.service.spec.ts create mode 100644 src/modules/admin/profile/profile.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index e8f2327a..e1ab14ec 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,7 +8,7 @@ import { MentorModule } from './modules/mentor/mentor.module'; import { ChatModule } from './modules/chat/chat.module'; import { RabbitmqModule } from './common/rabbitmq/rabbitmq.module'; import { MessageModule } from './modules/message/message.module'; -import { AdminProfileModule } from './modules/admin/adminProfile/admin-profile.module'; +import { ProfileModule } from './modules/admin/profile/profile.module'; import { RoleModule } from './modules/admin/role/role.module'; @Module({ @@ -27,7 +27,7 @@ import { RoleModule } from './modules/admin/role/role.module'; ChatModule, RabbitmqModule, MessageModule, - AdminProfileModule, + ProfileModule, RoleModule, ], providers: [PrismaService], diff --git a/src/modules/admin/profile/dto/profile.dto.ts b/src/modules/admin/profile/dto/profile.dto.ts new file mode 100644 index 00000000..84f2851a --- /dev/null +++ b/src/modules/admin/profile/dto/profile.dto.ts @@ -0,0 +1,34 @@ +// src/posts/dto/post.dto.ts + +import { Expose } from 'class-transformer'; + +export class AdminDto { + @Expose() + id: string; + + @Expose() + name: string; + + @Expose() + email: string; + + @Expose() + isActive: boolean; + + @Expose() + AccountUser: any; + + @Expose() + roleName: string; // Add the roleName field + + constructor(partial: Partial) { + // console.log('partial', partial); + Object.assign(this, { + id: partial.id, + name: partial.name, + email: partial.email, + isActive: partial.isActive, + AccountUser: partial.AccountUser, + }); + } +} diff --git a/src/modules/admin/profile/dto/update-profile.dto.ts b/src/modules/admin/profile/dto/update-profile.dto.ts new file mode 100644 index 00000000..322191ed --- /dev/null +++ b/src/modules/admin/profile/dto/update-profile.dto.ts @@ -0,0 +1,15 @@ +import { IsOptional, IsString } from 'class-validator'; + +export class UpdateAdminProfileDto { + @IsOptional() + @IsString() + name?: string; + + @IsOptional() + @IsString() + email?: string; + + @IsOptional() + @IsString() + password?: string; +} diff --git a/src/modules/admin/profile/entities/profile.entity.ts b/src/modules/admin/profile/entities/profile.entity.ts new file mode 100644 index 00000000..fb474a13 --- /dev/null +++ b/src/modules/admin/profile/entities/profile.entity.ts @@ -0,0 +1 @@ +export class AdminProfile {} diff --git a/src/modules/admin/profile/profile.controller.spec.ts b/src/modules/admin/profile/profile.controller.spec.ts new file mode 100644 index 00000000..54d95f68 --- /dev/null +++ b/src/modules/admin/profile/profile.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ProfileController } from './profile.controller'; +import { ProfileService } from './profile.service'; + +describe('AdminProfileController', () => { + let controller: ProfileController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ProfileController], + providers: [ProfileService], + }).compile(); + + controller = module.get(ProfileController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/modules/admin/profile/profile.controller.ts b/src/modules/admin/profile/profile.controller.ts new file mode 100644 index 00000000..b60fae59 --- /dev/null +++ b/src/modules/admin/profile/profile.controller.ts @@ -0,0 +1,49 @@ +import { Controller, Get, Body, Patch, Param, UseGuards } from '@nestjs/common'; +import { AuthGuard } from 'src/modules/auth/guard/auth/auth.guard'; +import { ProfileService } from './profile.service'; +import { UpdateAdminProfileDto } from './dto/update-profile.dto'; +import { RoleGuard } from 'src/modules/auth/guard/role/role.guard'; +import { Roles } from '../../auth/auth.decorator'; + +@Controller('admin/profile') +@UseGuards(AuthGuard) +export class ProfileController { + constructor(private readonly adminProfileService: ProfileService) {} + + @Get(':accountId/:userId/') + @UseGuards(RoleGuard) + @Roles('OWNER', 'ADMIN') + findOne( + @Param('userId') userId: string, + @Param('accountId') accountId: string, + ) { + return this.adminProfileService.findOne(userId, accountId); + } + + @Patch(':id') + @UseGuards(RoleGuard) + @Roles('OWNER', 'ADMIN') + update( + @Param('id') id: string, + @Body() updateAdminProfileDto: UpdateAdminProfileDto, + ) { + return this.adminProfileService.update(id, updateAdminProfileDto); + } + + @Get(':accountId') + @UseGuards(RoleGuard) + @Roles('OWNER') + findAll(@Param('accountId') accountId) { + return this.adminProfileService.findAll(accountId); + } + + @Patch(':userId/activate/:accountId') + @UseGuards(RoleGuard) + @Roles('OWNER') + activate( + @Param('accountId') accountId: string, + @Param('userId') userId: string, + ) { + return this.adminProfileService.toggleActiveStatus(accountId, userId); + } +} diff --git a/src/modules/admin/profile/profile.module.ts b/src/modules/admin/profile/profile.module.ts new file mode 100644 index 00000000..53331c9f --- /dev/null +++ b/src/modules/admin/profile/profile.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { ProfileService } from './profile.service'; +import { ProfileController } from './profile.controller'; +import { PrismaService } from '../../prisma/prisma.service'; +import { JwtService } from '@nestjs/jwt'; + +@Module({ + controllers: [ProfileController], + providers: [ProfileService, PrismaService, JwtService], +}) +export class ProfileModule {} diff --git a/src/modules/admin/profile/profile.service.spec.ts b/src/modules/admin/profile/profile.service.spec.ts new file mode 100644 index 00000000..c8bb6725 --- /dev/null +++ b/src/modules/admin/profile/profile.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ProfileService } from './profile.service'; + +describe('ProfileService', () => { + let service: ProfileService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ProfileService], + }).compile(); + + service = module.get(ProfileService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/modules/admin/profile/profile.service.ts b/src/modules/admin/profile/profile.service.ts new file mode 100644 index 00000000..819f2441 --- /dev/null +++ b/src/modules/admin/profile/profile.service.ts @@ -0,0 +1,167 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from 'src/modules/prisma/prisma.service'; +import { AdminDto } from './dto/profile.dto'; +import { UpdateAdminProfileDto } from './dto/update-profile.dto'; + +@Injectable() +export class ProfileService { + constructor(private prisma: PrismaService) {} + + async findOne(userId: string, accountId: string): Promise { + const admin = await this.prisma.user.findFirst({ + where: { + id: userId, + deletedAt: null, + }, + include: { + AccountUser: { + where: { + accountId: accountId, + }, + }, + }, + }); + + if (!admin) { + throw new NotFoundException('Admin not found'); + } + + const accountUser = admin.AccountUser.find( + (au) => au.accountId === accountId, + ); + + return new AdminDto({ + id: admin.id, + name: admin.name, + email: admin.email, + isActive: accountUser?.isActive, + }); + } + + async update( + id: string, + updateAdminProfileDto: UpdateAdminProfileDto, + ): Promise { + console.log('Updating admin with ID:', id); + + const existingAdmin = await this.prisma.user.findFirst({ + where: { + id: id, + deletedAt: null, + }, + }); + + if (!existingAdmin) { + throw new NotFoundException('Admin not found'); + } + + const updatedAdmin = await this.prisma.user.update({ + where: { id: id }, + data: { + ...updateAdminProfileDto, + }, + }); + + console.log('Updated admin:', updatedAdmin); + + return new AdminDto({ ...updatedAdmin }); + } + + async findAll(accountId: string): Promise { + const admins = await this.prisma.user.findMany({ + where: { + deletedAt: null, + OR: [ + { + AccountUser: { + some: { + accountId: accountId, + Role: { + name: 'Admin', + }, + }, + }, + }, + { + AccountUser: { + some: { + accountId: accountId, + Role: { + name: 'Owner', + }, + }, + }, + }, + ], + }, + include: { + AccountUser: { + include: { + Role: true, + }, + }, + }, + }); + + console.log('Filtered Admin Data:', JSON.stringify(admins, null, 2)); + + return admins.map((admin) => { + const accountUser = admin.AccountUser.find( + (au) => au.accountId === accountId, + ); + + return new AdminDto({ + id: admin.id, + name: admin.name, + email: admin.email, + isActive: accountUser?.isActive, + }); + }); + } + + async toggleActiveStatus( + accountId: string, + userId: string, + ): Promise { + const accountUser = await this.prisma.accountUser.findFirst({ + where: { + accountId: accountId, + userId: userId, + }, + }); + + const updatedAccountUser = await this.prisma.accountUser.update({ + where: { + id: accountUser.id, + }, + data: { + isActive: !accountUser.isActive, + }, + }); + + const updatedUser = await this.prisma.user.findFirst({ + where: { + id: userId, + deletedAt: null, + }, + include: { + AccountUser: { + where: { + accountId: accountId, + }, + }, + }, + }); + + if (!updatedUser) { + throw new NotFoundException('User not found'); + } + + return new AdminDto({ + id: updatedUser.id, + name: updatedUser.name, + email: updatedUser.email, + isActive: updatedAccountUser.isActive, + }); + } +} From 4765c493bd8dc9f763f73aa7cf9ae0ab16eb3716 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Tue, 1 Apr 2025 10:44:25 +0300 Subject: [PATCH 21/43] refactor: rename AdminDto to ProfileDto and update roleName to role --- src/modules/admin/profile/dto/profile.dto.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/modules/admin/profile/dto/profile.dto.ts b/src/modules/admin/profile/dto/profile.dto.ts index 84f2851a..46783a4e 100644 --- a/src/modules/admin/profile/dto/profile.dto.ts +++ b/src/modules/admin/profile/dto/profile.dto.ts @@ -1,8 +1,9 @@ // src/posts/dto/post.dto.ts +import { ro } from '@faker-js/faker/.'; import { Expose } from 'class-transformer'; -export class AdminDto { +export class ProfileDto { @Expose() id: string; @@ -19,14 +20,15 @@ export class AdminDto { AccountUser: any; @Expose() - roleName: string; // Add the roleName field + role: string; // Add the roleName field - constructor(partial: Partial) { + constructor(partial: Partial) { // console.log('partial', partial); Object.assign(this, { id: partial.id, name: partial.name, email: partial.email, + //role: partial.AccountUser.Role.name, isActive: partial.isActive, AccountUser: partial.AccountUser, }); From 6d76a6c9d15a305b6877d6f4d701233fc4314dbe Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Tue, 1 Apr 2025 10:50:41 +0300 Subject: [PATCH 22/43] fix es lint --- src/modules/admin/profile/dto/profile.dto.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/modules/admin/profile/dto/profile.dto.ts b/src/modules/admin/profile/dto/profile.dto.ts index 46783a4e..e96d7177 100644 --- a/src/modules/admin/profile/dto/profile.dto.ts +++ b/src/modules/admin/profile/dto/profile.dto.ts @@ -1,6 +1,5 @@ // src/posts/dto/post.dto.ts -import { ro } from '@faker-js/faker/.'; import { Expose } from 'class-transformer'; export class ProfileDto { @@ -20,10 +19,9 @@ export class ProfileDto { AccountUser: any; @Expose() - role: string; // Add the roleName field + role: string; constructor(partial: Partial) { - // console.log('partial', partial); Object.assign(this, { id: partial.id, name: partial.name, From 14c82bb11a4c5902a8eef6cc3a716add758b4717 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Tue, 1 Apr 2025 10:53:10 +0300 Subject: [PATCH 23/43] refactor: remove unnecessary comment from ProfileDto file --- src/modules/admin/profile/dto/profile.dto.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/modules/admin/profile/dto/profile.dto.ts b/src/modules/admin/profile/dto/profile.dto.ts index e96d7177..f2ed257f 100644 --- a/src/modules/admin/profile/dto/profile.dto.ts +++ b/src/modules/admin/profile/dto/profile.dto.ts @@ -1,5 +1,3 @@ -// src/posts/dto/post.dto.ts - import { Expose } from 'class-transformer'; export class ProfileDto { From ef70806683f7972561e7792cf73765ff414e978b Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Tue, 1 Apr 2025 11:15:15 +0300 Subject: [PATCH 24/43] refactor: update ProfileService to use ProfileDto and add RoleDto for role management --- src/modules/admin/profile/profile.service.ts | 18 +++++++------- src/modules/admin/role/dto/role.dto.ts | 20 ++++++++++++++++ src/modules/admin/role/role.service.ts | 25 ++++---------------- 3 files changed, 33 insertions(+), 30 deletions(-) create mode 100644 src/modules/admin/role/dto/role.dto.ts diff --git a/src/modules/admin/profile/profile.service.ts b/src/modules/admin/profile/profile.service.ts index 819f2441..a1ea126a 100644 --- a/src/modules/admin/profile/profile.service.ts +++ b/src/modules/admin/profile/profile.service.ts @@ -1,13 +1,13 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from 'src/modules/prisma/prisma.service'; -import { AdminDto } from './dto/profile.dto'; +import { ProfileDto } from './dto/profile.dto'; import { UpdateAdminProfileDto } from './dto/update-profile.dto'; @Injectable() export class ProfileService { constructor(private prisma: PrismaService) {} - async findOne(userId: string, accountId: string): Promise { + async findOne(userId: string, accountId: string): Promise { const admin = await this.prisma.user.findFirst({ where: { id: userId, @@ -30,7 +30,7 @@ export class ProfileService { (au) => au.accountId === accountId, ); - return new AdminDto({ + return new ProfileDto({ id: admin.id, name: admin.name, email: admin.email, @@ -41,7 +41,7 @@ export class ProfileService { async update( id: string, updateAdminProfileDto: UpdateAdminProfileDto, - ): Promise { + ): Promise { console.log('Updating admin with ID:', id); const existingAdmin = await this.prisma.user.findFirst({ @@ -64,10 +64,10 @@ export class ProfileService { console.log('Updated admin:', updatedAdmin); - return new AdminDto({ ...updatedAdmin }); + return new ProfileDto({ ...updatedAdmin }); } - async findAll(accountId: string): Promise { + async findAll(accountId: string): Promise { const admins = await this.prisma.user.findMany({ where: { deletedAt: null, @@ -110,7 +110,7 @@ export class ProfileService { (au) => au.accountId === accountId, ); - return new AdminDto({ + return new ProfileDto({ id: admin.id, name: admin.name, email: admin.email, @@ -122,7 +122,7 @@ export class ProfileService { async toggleActiveStatus( accountId: string, userId: string, - ): Promise { + ): Promise { const accountUser = await this.prisma.accountUser.findFirst({ where: { accountId: accountId, @@ -157,7 +157,7 @@ export class ProfileService { throw new NotFoundException('User not found'); } - return new AdminDto({ + return new ProfileDto({ id: updatedUser.id, name: updatedUser.name, email: updatedUser.email, diff --git a/src/modules/admin/role/dto/role.dto.ts b/src/modules/admin/role/dto/role.dto.ts new file mode 100644 index 00000000..f24fe4b3 --- /dev/null +++ b/src/modules/admin/role/dto/role.dto.ts @@ -0,0 +1,20 @@ +import { Expose } from 'class-transformer'; + +export class RoleDto { + @Expose() + id: string; + + @Expose() + name: string; + + @Expose() + accountId: string; + + constructor(partial: Partial) { + Object.assign(this, { + id: partial.id, + name: partial.name, + accountId: partial.accountId, + }); + } +} diff --git a/src/modules/admin/role/role.service.ts b/src/modules/admin/role/role.service.ts index d51dfbc9..e2c4655c 100644 --- a/src/modules/admin/role/role.service.ts +++ b/src/modules/admin/role/role.service.ts @@ -1,17 +1,13 @@ import { Injectable } from '@nestjs/common'; -//import { CreateRoleDto } from './dto/create-role.dto'; -//import { UpdateRoleDto } from './dto/update-role.dto'; import { PrismaService } from '../../prisma/prisma.service'; +import { RoleDto } from './dto/role.dto'; @Injectable() export class RoleService { constructor(private readonly prisma: PrismaService) {} - /* create(createRoleDto: CreateRoleDto) { - return 'This action adds a new role'; - } */ - findAll(accountId: string) { - return this.prisma.role.findMany({ + async findAll(accountId: string): Promise { + const roles = await this.prisma.role.findMany({ where: { accountId: accountId, }, @@ -19,21 +15,8 @@ export class RoleService { id: true, name: true, accountId: true, - createdAt: true, - updatedAt: true, }, }); - } - - findOne(id: number) { - return `This action returns a #${id} role`; - } - - /* update(id: number, updateRoleDto: UpdateRoleDto) { - return `This action updates a #${id} role`; - } */ - - remove(id: number) { - return `This action removes a #${id} role`; + return roles.map(role => new RoleDto(role)); } } From e0c87ce6f3de76867f3f0ab9a6ad773f05e8dc44 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Tue, 1 Apr 2025 11:15:40 +0300 Subject: [PATCH 25/43] refactor: format code in RoleService for consistency --- src/modules/admin/role/role.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/admin/role/role.service.ts b/src/modules/admin/role/role.service.ts index e2c4655c..3a3c801d 100644 --- a/src/modules/admin/role/role.service.ts +++ b/src/modules/admin/role/role.service.ts @@ -17,6 +17,6 @@ export class RoleService { accountId: true, }, }); - return roles.map(role => new RoleDto(role)); + return roles.map((role) => new RoleDto(role)); } } From 7316949cb6dc19e072dd674ea2b7bbd9c37c50e5 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Tue, 1 Apr 2025 11:21:10 +0300 Subject: [PATCH 26/43] refactor: rename RoleController to RolesController for consistency --- src/modules/admin/role/role.controller.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/modules/admin/role/role.controller.spec.ts b/src/modules/admin/role/role.controller.spec.ts index 7853f98b..40763c60 100644 --- a/src/modules/admin/role/role.controller.spec.ts +++ b/src/modules/admin/role/role.controller.spec.ts @@ -1,17 +1,17 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { RoleController } from './role.controller'; +import { RolesController } from './role.controller'; import { RoleService } from './role.service'; -describe('RoleController', () => { - let controller: RoleController; +describe('RolesController', () => { + let controller: RolesController; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - controllers: [RoleController], + controllers: [RolesController], providers: [RoleService], }).compile(); - controller = module.get(RoleController); + controller = module.get(RolesController); }); it('should be defined', () => { From 5e5e9f12d87dade80abd47bdfa9511ddf61232a9 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Tue, 1 Apr 2025 11:42:37 +0300 Subject: [PATCH 27/43] refactor: enhance ProfileService to filter by Admin role in account user query --- src/modules/admin/profile/profile.service.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/modules/admin/profile/profile.service.ts b/src/modules/admin/profile/profile.service.ts index a1ea126a..22ef0e7e 100644 --- a/src/modules/admin/profile/profile.service.ts +++ b/src/modules/admin/profile/profile.service.ts @@ -12,6 +12,14 @@ export class ProfileService { where: { id: userId, deletedAt: null, + AccountUser: { + some: { + accountId: accountId, + Role: { + name: 'Admin', + }, + }, + }, }, include: { AccountUser: { From b61149e10597ccfb8bd659e09413c89df7f712c1 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Tue, 1 Apr 2025 16:33:07 +0300 Subject: [PATCH 28/43] removed the profile module and moved all logics to the user in admin module --- src/app.module.ts | 2 - .../admin/user/dto/get-all-users-query.dto.ts | 24 +++++ src/modules/admin/user/dto/user.dto.ts | 6 +- src/modules/admin/user/user.controller.ts | 33 ++++++- src/modules/admin/user/user.service.ts | 88 ++++++++++++++++--- 5 files changed, 134 insertions(+), 19 deletions(-) create mode 100644 src/modules/admin/user/dto/get-all-users-query.dto.ts diff --git a/src/app.module.ts b/src/app.module.ts index e1ab14ec..fc174e54 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,7 +8,6 @@ import { MentorModule } from './modules/mentor/mentor.module'; import { ChatModule } from './modules/chat/chat.module'; import { RabbitmqModule } from './common/rabbitmq/rabbitmq.module'; import { MessageModule } from './modules/message/message.module'; -import { ProfileModule } from './modules/admin/profile/profile.module'; import { RoleModule } from './modules/admin/role/role.module'; @Module({ @@ -27,7 +26,6 @@ import { RoleModule } from './modules/admin/role/role.module'; ChatModule, RabbitmqModule, MessageModule, - ProfileModule, RoleModule, ], providers: [PrismaService], diff --git a/src/modules/admin/user/dto/get-all-users-query.dto.ts b/src/modules/admin/user/dto/get-all-users-query.dto.ts new file mode 100644 index 00000000..22e66509 --- /dev/null +++ b/src/modules/admin/user/dto/get-all-users-query.dto.ts @@ -0,0 +1,24 @@ +import { IsInt, IsOptional, Min, IsString } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class GetAllUsersQueryDto { + @IsOptional() + @IsString() + accountId?: string; + + @IsOptional() + @IsString() + roleId?: string; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page?: number = 1; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + limit?: number = 10; +} diff --git a/src/modules/admin/user/dto/user.dto.ts b/src/modules/admin/user/dto/user.dto.ts index 7ead2a95..88136458 100644 --- a/src/modules/admin/user/dto/user.dto.ts +++ b/src/modules/admin/user/dto/user.dto.ts @@ -13,19 +13,21 @@ export class UserDto { @Expose() imageUrl: string; + @Expose() + role?: string; + @Expose() createdAt: Date; @Expose() updatedAt: Date; - roles?: string[]; - constructor(partial: Partial) { this.id = partial.id; this.name = partial.name; this.email = partial.email; this.imageUrl = partial.imageUrl; + this.role = partial.role; this.createdAt = partial.createdAt; this.updatedAt = partial.updatedAt; } diff --git a/src/modules/admin/user/user.controller.ts b/src/modules/admin/user/user.controller.ts index 86198c93..86b53c5f 100644 --- a/src/modules/admin/user/user.controller.ts +++ b/src/modules/admin/user/user.controller.ts @@ -7,35 +7,48 @@ import { Patch, Post, UseGuards, + Query, + ValidationPipe, } from '@nestjs/common'; import { UserService } from './user.service'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { AuthGuard } from 'src/modules/auth/guard/auth/auth.guard'; +import { RoleGuard } from 'src/modules/auth/guard/role/role.guard'; import { Roles } from 'src/modules/auth/auth.decorator'; +import { GetAllUsersQueryDto } from './dto/get-all-users-query.dto'; @Controller('admin/user') @UseGuards(AuthGuard) -@Roles('OWNER') export class UserController { constructor(private readonly userService: UserService) {} @Post() + @UseGuards(RoleGuard) + @Roles('OWNER', 'ADMIN') create(@Body() createUserDto: CreateUserDto) { return this.userService.create(createUserDto); } - @Get(':accountId/all') - async findAll(@Param('accountId') accountId: string) { - return this.userService.findAllUsers(accountId); + @Get('all') + @UseGuards(RoleGuard) + @Roles('OWNER') + async findAll( + @Query(new ValidationPipe({ transform: true })) query: GetAllUsersQueryDto, + ) { + return this.userService.findAllUsers(query); } @Get(':accountId/user/:id') + @UseGuards(RoleGuard) + @Roles('OWNER', 'ADMIN') findOne(@Param('accountId') accountId: string, @Param('id') id: string) { return this.userService.findOne(accountId, id); } @Patch(':accountId/user/:id') + @UseGuards(RoleGuard) + @Roles('OWNER', 'ADMIN') update( @Param('accountId') accountId: string, @Param('id') id: string, @@ -45,7 +58,19 @@ export class UserController { } @Delete(':accountId/user/:id') + @UseGuards(RoleGuard) + @Roles('OWNER') remove(@Param('accountId') accountId: string, @Param('id') id: string) { return this.userService.remove(accountId, id); } + + @Patch(':userId/activate/:accountId') + @UseGuards(RoleGuard) + @Roles('OWNER') + activate( + @Param('accountId') accountId: string, + @Param('userId') userId: string, + ) { + return this.userService.toggleActiveStatus(userId, accountId); + } } diff --git a/src/modules/admin/user/user.service.ts b/src/modules/admin/user/user.service.ts index ad5f4a15..a975f986 100644 --- a/src/modules/admin/user/user.service.ts +++ b/src/modules/admin/user/user.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable, ForbiddenException } from '@nestjs/common'; +import { Inject, ForbiddenException } from '@nestjs/common'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { PrismaService } from 'src/modules/prisma/prisma.service'; @@ -6,8 +6,9 @@ import { UserDto } from './dto/user.dto'; import { REQUEST } from '@nestjs/core'; import * as bcrypt from 'bcryptjs'; import { AuthService } from 'src/modules/auth/auth.service'; - -@Injectable() +import { paginate } from 'src/common/helpers/pagination'; +import { User, AccountUser, Role } from '@prisma/client'; // Assuming User is from Prisma model@Injectable() +import { GetAllUsersQueryDto } from './dto/get-all-users-query.dto'; export class UserService { constructor( @Inject(REQUEST) private readonly request: any, @@ -81,18 +82,60 @@ export class UserService { return new UserDto(userData); } - async findAllUsers(accountId: string) { + async findAllUsers(query: GetAllUsersQueryDto) { + const { accountId, roleId, page, limit } = query; + await this.validateAccountAccess(accountId); - return this.prisma.user.findMany({ - where: { - AccountUser: { - some: { - accountId: accountId, - }, + const whereCondition = { + AccountUser: { + some: { + accountId: accountId, + ...(roleId ? { roleId: roleId } : {}), }, }, - }); + }; + + const includeCondition = { + AccountUser: { + include: { + Role: true, + }, + }, + }; + + const { data, meta } = await paginate( + this.prisma, + this.prisma.user, + whereCondition, + page, + limit, + includeCondition, + ); + + const users = data.map( + (user: User & { AccountUser: (AccountUser & { Role: Role })[] }) => { + const role = + user.AccountUser.length > 0 && user.AccountUser[0].Role + ? user.AccountUser[0].Role.name + : ''; + + return new UserDto({ + id: user.id, + name: user.name, + email: user.email, + imageUrl: user.imageUrl, + role: role, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + }); + }, + ); + + return { + data: users, + meta: meta, + }; } async updateUser( @@ -157,4 +200,27 @@ export class UserService { return account; } + + async toggleActiveStatus(userId: string, accountId: string) { + const accountUser = await this.prisma.accountUser.findFirst({ + where: { + accountId: accountId, + userId: userId, + }, + }); + + const updatedStatus = await this.prisma.accountUser.update({ + where: { + id: accountUser.id, + }, + data: { + isActive: !accountUser.isActive, + }, + }); + + return { + message: updatedStatus.isActive ? 'Activated' : 'Deactivated', + isActive: updatedStatus.isActive, + }; + } } From 285b592c99e971ad881d393d341058922403e188 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Tue, 1 Apr 2025 16:36:05 +0300 Subject: [PATCH 29/43] removed the admin profile --- .../20250218072627_init/migration.sql | 168 ----------------- .../admin-profile.controller.spec.ts | 20 -- .../adminProfile/admin-profile.controller.ts | 49 ----- .../adminProfile/admin-profile.module.ts | 11 -- .../admin-profile.service.spec.ts | 18 -- .../adminProfile/admin-profile.service.ts | 167 ----------------- .../admin/adminProfile/dto/admin.dto.ts | 34 ---- .../dto/update-admin-profile.dto.ts | 15 -- .../entities/admin-profile.entity.ts | 1 - src/modules/admin/profile/dto/profile.dto.ts | 32 ---- .../admin/profile/dto/update-profile.dto.ts | 15 -- .../admin/profile/entities/profile.entity.ts | 1 - .../admin/profile/profile.controller.spec.ts | 20 -- .../admin/profile/profile.controller.ts | 49 ----- src/modules/admin/profile/profile.module.ts | 11 -- .../admin/profile/profile.service.spec.ts | 18 -- src/modules/admin/profile/profile.service.ts | 175 ------------------ 17 files changed, 804 deletions(-) delete mode 100644 prisma/migrations/20250218072627_init/migration.sql delete mode 100644 src/modules/admin/adminProfile/admin-profile.controller.spec.ts delete mode 100644 src/modules/admin/adminProfile/admin-profile.controller.ts delete mode 100644 src/modules/admin/adminProfile/admin-profile.module.ts delete mode 100644 src/modules/admin/adminProfile/admin-profile.service.spec.ts delete mode 100644 src/modules/admin/adminProfile/admin-profile.service.ts delete mode 100644 src/modules/admin/adminProfile/dto/admin.dto.ts delete mode 100644 src/modules/admin/adminProfile/dto/update-admin-profile.dto.ts delete mode 100644 src/modules/admin/adminProfile/entities/admin-profile.entity.ts delete mode 100644 src/modules/admin/profile/dto/profile.dto.ts delete mode 100644 src/modules/admin/profile/dto/update-profile.dto.ts delete mode 100644 src/modules/admin/profile/entities/profile.entity.ts delete mode 100644 src/modules/admin/profile/profile.controller.spec.ts delete mode 100644 src/modules/admin/profile/profile.controller.ts delete mode 100644 src/modules/admin/profile/profile.module.ts delete mode 100644 src/modules/admin/profile/profile.service.spec.ts delete mode 100644 src/modules/admin/profile/profile.service.ts diff --git a/prisma/migrations/20250218072627_init/migration.sql b/prisma/migrations/20250218072627_init/migration.sql deleted file mode 100644 index bb601f08..00000000 --- a/prisma/migrations/20250218072627_init/migration.sql +++ /dev/null @@ -1,168 +0,0 @@ --- CreateEnum -CREATE TYPE "RoleType" AS ENUM ('OWNER', 'ADMIN', 'USER', 'MENTOR'); - --- CreateEnum -CREATE TYPE "GenderType" AS ENUM ('MALE', 'FEMALE'); - --- CreateEnum -CREATE TYPE "ChannelType" AS ENUM ('NEGARIT', 'TELEGRAM', 'WHATSAPP', 'TWILIO'); - --- CreateEnum -CREATE TYPE "MessageType" AS ENUM ('SENT', 'RECEIVED'); - --- CreateTable -CREATE TABLE "Account" ( - "id" TEXT NOT NULL, - "name" TEXT NOT NULL, - "domain" TEXT, - "deletedAt" TIMESTAMP(3), - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Account_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "User" ( - "id" TEXT NOT NULL, - "name" TEXT NOT NULL, - "email" TEXT NOT NULL, - "password" TEXT NOT NULL, - "imageUrl" TEXT, - "deletedAt" TIMESTAMP(3), - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "User_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "AccountUser" ( - "id" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "roleId" TEXT NOT NULL, - "accountId" TEXT NOT NULL, - "deletedAt" TIMESTAMP(3), - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "AccountUser_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Mentor" ( - "id" TEXT NOT NULL, - "accountId" TEXT NOT NULL, - "name" TEXT NOT NULL, - "email" TEXT NOT NULL, - "expertise" TEXT, - "availability" JSONB, - "age" INTEGER, - "gender" "GenderType" NOT NULL DEFAULT 'MALE', - "location" TEXT, - "isActive" BOOLEAN NOT NULL DEFAULT true, - "deletedAt" TIMESTAMP(3), - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Mentor_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Conversation" ( - "id" TEXT NOT NULL, - "mentorId" TEXT NOT NULL, - "address" TEXT NOT NULL, - "channelId" TEXT NOT NULL, - "isActive" BOOLEAN NOT NULL, - - CONSTRAINT "Conversation_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Role" ( - "id" TEXT NOT NULL, - "name" TEXT NOT NULL, - "isDefault" BOOLEAN NOT NULL DEFAULT false, - "type" "RoleType" NOT NULL, - "accountId" TEXT, - "deletedAt" TIMESTAMP(3), - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Role_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Thread" ( - "id" TEXT NOT NULL, - "conversationId" TEXT NOT NULL, - "messageId" TEXT NOT NULL, - - CONSTRAINT "Thread_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Channel" ( - "id" TEXT NOT NULL, - "accountId" TEXT NOT NULL, - "name" TEXT NOT NULL, - "type" "ChannelType" NOT NULL DEFAULT 'NEGARIT', - "configuration" JSONB, - "isOn" BOOLEAN NOT NULL DEFAULT false, - "deletedAt" TIMESTAMP(3), - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Channel_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Message" ( - "id" TEXT NOT NULL, - "channelId" TEXT NOT NULL, - "address" TEXT NOT NULL, - "type" "MessageType" NOT NULL, - "body" TEXT NOT NULL, - "deletedAt" TIMESTAMP(3), - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Message_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); - --- AddForeignKey -ALTER TABLE "AccountUser" ADD CONSTRAINT "AccountUser_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "AccountUser" ADD CONSTRAINT "AccountUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "AccountUser" ADD CONSTRAINT "AccountUser_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Mentor" ADD CONSTRAINT "Mentor_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Conversation" ADD CONSTRAINT "Conversation_mentorId_fkey" FOREIGN KEY ("mentorId") REFERENCES "Mentor"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Conversation" ADD CONSTRAINT "Conversation_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "Channel"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Role" ADD CONSTRAINT "Role_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Thread" ADD CONSTRAINT "Thread_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "Conversation"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Thread" ADD CONSTRAINT "Thread_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "Message"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Channel" ADD CONSTRAINT "Channel_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Message" ADD CONSTRAINT "Message_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "Channel"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/src/modules/admin/adminProfile/admin-profile.controller.spec.ts b/src/modules/admin/adminProfile/admin-profile.controller.spec.ts deleted file mode 100644 index 116acd12..00000000 --- a/src/modules/admin/adminProfile/admin-profile.controller.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AdminProfileController } from './admin-profile.controller'; -import { AdminProfileService } from './admin-profile.service'; - -describe('AdminProfileController', () => { - let controller: AdminProfileController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [AdminProfileController], - providers: [AdminProfileService], - }).compile(); - - controller = module.get(AdminProfileController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/src/modules/admin/adminProfile/admin-profile.controller.ts b/src/modules/admin/adminProfile/admin-profile.controller.ts deleted file mode 100644 index f97b57d6..00000000 --- a/src/modules/admin/adminProfile/admin-profile.controller.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Controller, Get, Body, Patch, Param, UseGuards } from '@nestjs/common'; -import { AuthGuard } from 'src/modules/auth/guard/auth/auth.guard'; -import { AdminProfileService } from './admin-profile.service'; -import { UpdateAdminProfileDto } from './dto/update-admin-profile.dto'; -import { RoleGuard } from 'src/modules/auth/guard/role/role.guard'; -import { Roles } from '../../auth/auth.decorator'; - -@Controller('admin/profile') -@UseGuards(AuthGuard) -export class AdminProfileController { - constructor(private readonly adminProfileService: AdminProfileService) {} - - @Get(':accountId/:userId/') - @UseGuards(RoleGuard) - @Roles('OWNER', 'ADMIN') - findOne( - @Param('userId') userId: string, - @Param('accountId') accountId: string, - ) { - return this.adminProfileService.findOne(userId, accountId); - } - - @Patch(':id') - @UseGuards(RoleGuard) - @Roles('OWNER', 'ADMIN') - update( - @Param('id') id: string, - @Body() updateAdminProfileDto: UpdateAdminProfileDto, - ) { - return this.adminProfileService.update(id, updateAdminProfileDto); - } - - @Get(':accountId') - @UseGuards(RoleGuard) - @Roles('OWNER') - findAll(@Param('accountId') accountId) { - return this.adminProfileService.findAll(accountId); - } - - @Patch(':userId/activate/:accountId') - @UseGuards(RoleGuard) - @Roles('OWNER') - activate( - @Param('accountId') accountId: string, - @Param('userId') userId: string, - ) { - return this.adminProfileService.toggleActiveStatus(accountId, userId); - } -} diff --git a/src/modules/admin/adminProfile/admin-profile.module.ts b/src/modules/admin/adminProfile/admin-profile.module.ts deleted file mode 100644 index 705d96ec..00000000 --- a/src/modules/admin/adminProfile/admin-profile.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; -import { AdminProfileService } from './admin-profile.service'; -import { AdminProfileController } from './admin-profile.controller'; -import { PrismaService } from '../../prisma/prisma.service'; -import { JwtService } from '@nestjs/jwt'; - -@Module({ - controllers: [AdminProfileController], - providers: [AdminProfileService, PrismaService, JwtService], -}) -export class AdminProfileModule {} diff --git a/src/modules/admin/adminProfile/admin-profile.service.spec.ts b/src/modules/admin/adminProfile/admin-profile.service.spec.ts deleted file mode 100644 index 2c85e333..00000000 --- a/src/modules/admin/adminProfile/admin-profile.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AdminProfileService } from './admin-profile.service'; - -describe('AdminProfileService', () => { - let service: AdminProfileService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [AdminProfileService], - }).compile(); - - service = module.get(AdminProfileService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/src/modules/admin/adminProfile/admin-profile.service.ts b/src/modules/admin/adminProfile/admin-profile.service.ts deleted file mode 100644 index 39a9dbe3..00000000 --- a/src/modules/admin/adminProfile/admin-profile.service.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { PrismaService } from 'src/modules/prisma/prisma.service'; -import { AdminDto } from './dto/admin.dto'; -import { UpdateAdminProfileDto } from './dto/update-admin-profile.dto'; - -@Injectable() -export class AdminProfileService { - constructor(private prisma: PrismaService) {} - - async findOne(userId: string, accountId: string): Promise { - const admin = await this.prisma.user.findFirst({ - where: { - id: userId, - deletedAt: null, - }, - include: { - AccountUser: { - where: { - accountId: accountId, - }, - }, - }, - }); - - if (!admin) { - throw new NotFoundException('Admin not found'); - } - - const accountUser = admin.AccountUser.find( - (au) => au.accountId === accountId, - ); - - return new AdminDto({ - id: admin.id, - name: admin.name, - email: admin.email, - isActive: accountUser?.isActive, - }); - } - - async update( - id: string, - updateAdminProfileDto: UpdateAdminProfileDto, - ): Promise { - console.log('Updating admin with ID:', id); - - const existingAdmin = await this.prisma.user.findFirst({ - where: { - id: id, - deletedAt: null, - }, - }); - - if (!existingAdmin) { - throw new NotFoundException('Admin not found'); - } - - const updatedAdmin = await this.prisma.user.update({ - where: { id: id }, - data: { - ...updateAdminProfileDto, - }, - }); - - console.log('Updated admin:', updatedAdmin); - - return new AdminDto({ ...updatedAdmin }); - } - - async findAll(accountId: string): Promise { - const admins = await this.prisma.user.findMany({ - where: { - deletedAt: null, - OR: [ - { - AccountUser: { - some: { - accountId: accountId, - Role: { - name: 'Admin', - }, - }, - }, - }, - { - AccountUser: { - some: { - accountId: accountId, - Role: { - name: 'Owner', - }, - }, - }, - }, - ], - }, - include: { - AccountUser: { - include: { - Role: true, - }, - }, - }, - }); - - console.log('Filtered Admin Data:', JSON.stringify(admins, null, 2)); - - return admins.map((admin) => { - const accountUser = admin.AccountUser.find( - (au) => au.accountId === accountId, - ); - - return new AdminDto({ - id: admin.id, - name: admin.name, - email: admin.email, - isActive: accountUser?.isActive, - }); - }); - } - - async toggleActiveStatus( - accountId: string, - userId: string, - ): Promise { - const accountUser = await this.prisma.accountUser.findFirst({ - where: { - accountId: accountId, - userId: userId, - }, - }); - - const updatedAccountUser = await this.prisma.accountUser.update({ - where: { - id: accountUser.id, - }, - data: { - isActive: !accountUser.isActive, - }, - }); - - const updatedUser = await this.prisma.user.findFirst({ - where: { - id: userId, - deletedAt: null, - }, - include: { - AccountUser: { - where: { - accountId: accountId, - }, - }, - }, - }); - - if (!updatedUser) { - throw new NotFoundException('User not found'); - } - - return new AdminDto({ - id: updatedUser.id, - name: updatedUser.name, - email: updatedUser.email, - isActive: updatedAccountUser.isActive, - }); - } -} diff --git a/src/modules/admin/adminProfile/dto/admin.dto.ts b/src/modules/admin/adminProfile/dto/admin.dto.ts deleted file mode 100644 index 84f2851a..00000000 --- a/src/modules/admin/adminProfile/dto/admin.dto.ts +++ /dev/null @@ -1,34 +0,0 @@ -// src/posts/dto/post.dto.ts - -import { Expose } from 'class-transformer'; - -export class AdminDto { - @Expose() - id: string; - - @Expose() - name: string; - - @Expose() - email: string; - - @Expose() - isActive: boolean; - - @Expose() - AccountUser: any; - - @Expose() - roleName: string; // Add the roleName field - - constructor(partial: Partial) { - // console.log('partial', partial); - Object.assign(this, { - id: partial.id, - name: partial.name, - email: partial.email, - isActive: partial.isActive, - AccountUser: partial.AccountUser, - }); - } -} diff --git a/src/modules/admin/adminProfile/dto/update-admin-profile.dto.ts b/src/modules/admin/adminProfile/dto/update-admin-profile.dto.ts deleted file mode 100644 index 322191ed..00000000 --- a/src/modules/admin/adminProfile/dto/update-admin-profile.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { IsOptional, IsString } from 'class-validator'; - -export class UpdateAdminProfileDto { - @IsOptional() - @IsString() - name?: string; - - @IsOptional() - @IsString() - email?: string; - - @IsOptional() - @IsString() - password?: string; -} diff --git a/src/modules/admin/adminProfile/entities/admin-profile.entity.ts b/src/modules/admin/adminProfile/entities/admin-profile.entity.ts deleted file mode 100644 index fb474a13..00000000 --- a/src/modules/admin/adminProfile/entities/admin-profile.entity.ts +++ /dev/null @@ -1 +0,0 @@ -export class AdminProfile {} diff --git a/src/modules/admin/profile/dto/profile.dto.ts b/src/modules/admin/profile/dto/profile.dto.ts deleted file mode 100644 index f2ed257f..00000000 --- a/src/modules/admin/profile/dto/profile.dto.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Expose } from 'class-transformer'; - -export class ProfileDto { - @Expose() - id: string; - - @Expose() - name: string; - - @Expose() - email: string; - - @Expose() - isActive: boolean; - - @Expose() - AccountUser: any; - - @Expose() - role: string; - - constructor(partial: Partial) { - Object.assign(this, { - id: partial.id, - name: partial.name, - email: partial.email, - //role: partial.AccountUser.Role.name, - isActive: partial.isActive, - AccountUser: partial.AccountUser, - }); - } -} diff --git a/src/modules/admin/profile/dto/update-profile.dto.ts b/src/modules/admin/profile/dto/update-profile.dto.ts deleted file mode 100644 index 322191ed..00000000 --- a/src/modules/admin/profile/dto/update-profile.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { IsOptional, IsString } from 'class-validator'; - -export class UpdateAdminProfileDto { - @IsOptional() - @IsString() - name?: string; - - @IsOptional() - @IsString() - email?: string; - - @IsOptional() - @IsString() - password?: string; -} diff --git a/src/modules/admin/profile/entities/profile.entity.ts b/src/modules/admin/profile/entities/profile.entity.ts deleted file mode 100644 index fb474a13..00000000 --- a/src/modules/admin/profile/entities/profile.entity.ts +++ /dev/null @@ -1 +0,0 @@ -export class AdminProfile {} diff --git a/src/modules/admin/profile/profile.controller.spec.ts b/src/modules/admin/profile/profile.controller.spec.ts deleted file mode 100644 index 54d95f68..00000000 --- a/src/modules/admin/profile/profile.controller.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { ProfileController } from './profile.controller'; -import { ProfileService } from './profile.service'; - -describe('AdminProfileController', () => { - let controller: ProfileController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [ProfileController], - providers: [ProfileService], - }).compile(); - - controller = module.get(ProfileController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/src/modules/admin/profile/profile.controller.ts b/src/modules/admin/profile/profile.controller.ts deleted file mode 100644 index b60fae59..00000000 --- a/src/modules/admin/profile/profile.controller.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Controller, Get, Body, Patch, Param, UseGuards } from '@nestjs/common'; -import { AuthGuard } from 'src/modules/auth/guard/auth/auth.guard'; -import { ProfileService } from './profile.service'; -import { UpdateAdminProfileDto } from './dto/update-profile.dto'; -import { RoleGuard } from 'src/modules/auth/guard/role/role.guard'; -import { Roles } from '../../auth/auth.decorator'; - -@Controller('admin/profile') -@UseGuards(AuthGuard) -export class ProfileController { - constructor(private readonly adminProfileService: ProfileService) {} - - @Get(':accountId/:userId/') - @UseGuards(RoleGuard) - @Roles('OWNER', 'ADMIN') - findOne( - @Param('userId') userId: string, - @Param('accountId') accountId: string, - ) { - return this.adminProfileService.findOne(userId, accountId); - } - - @Patch(':id') - @UseGuards(RoleGuard) - @Roles('OWNER', 'ADMIN') - update( - @Param('id') id: string, - @Body() updateAdminProfileDto: UpdateAdminProfileDto, - ) { - return this.adminProfileService.update(id, updateAdminProfileDto); - } - - @Get(':accountId') - @UseGuards(RoleGuard) - @Roles('OWNER') - findAll(@Param('accountId') accountId) { - return this.adminProfileService.findAll(accountId); - } - - @Patch(':userId/activate/:accountId') - @UseGuards(RoleGuard) - @Roles('OWNER') - activate( - @Param('accountId') accountId: string, - @Param('userId') userId: string, - ) { - return this.adminProfileService.toggleActiveStatus(accountId, userId); - } -} diff --git a/src/modules/admin/profile/profile.module.ts b/src/modules/admin/profile/profile.module.ts deleted file mode 100644 index 53331c9f..00000000 --- a/src/modules/admin/profile/profile.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ProfileService } from './profile.service'; -import { ProfileController } from './profile.controller'; -import { PrismaService } from '../../prisma/prisma.service'; -import { JwtService } from '@nestjs/jwt'; - -@Module({ - controllers: [ProfileController], - providers: [ProfileService, PrismaService, JwtService], -}) -export class ProfileModule {} diff --git a/src/modules/admin/profile/profile.service.spec.ts b/src/modules/admin/profile/profile.service.spec.ts deleted file mode 100644 index c8bb6725..00000000 --- a/src/modules/admin/profile/profile.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { ProfileService } from './profile.service'; - -describe('ProfileService', () => { - let service: ProfileService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ProfileService], - }).compile(); - - service = module.get(ProfileService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/src/modules/admin/profile/profile.service.ts b/src/modules/admin/profile/profile.service.ts deleted file mode 100644 index 22ef0e7e..00000000 --- a/src/modules/admin/profile/profile.service.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { PrismaService } from 'src/modules/prisma/prisma.service'; -import { ProfileDto } from './dto/profile.dto'; -import { UpdateAdminProfileDto } from './dto/update-profile.dto'; - -@Injectable() -export class ProfileService { - constructor(private prisma: PrismaService) {} - - async findOne(userId: string, accountId: string): Promise { - const admin = await this.prisma.user.findFirst({ - where: { - id: userId, - deletedAt: null, - AccountUser: { - some: { - accountId: accountId, - Role: { - name: 'Admin', - }, - }, - }, - }, - include: { - AccountUser: { - where: { - accountId: accountId, - }, - }, - }, - }); - - if (!admin) { - throw new NotFoundException('Admin not found'); - } - - const accountUser = admin.AccountUser.find( - (au) => au.accountId === accountId, - ); - - return new ProfileDto({ - id: admin.id, - name: admin.name, - email: admin.email, - isActive: accountUser?.isActive, - }); - } - - async update( - id: string, - updateAdminProfileDto: UpdateAdminProfileDto, - ): Promise { - console.log('Updating admin with ID:', id); - - const existingAdmin = await this.prisma.user.findFirst({ - where: { - id: id, - deletedAt: null, - }, - }); - - if (!existingAdmin) { - throw new NotFoundException('Admin not found'); - } - - const updatedAdmin = await this.prisma.user.update({ - where: { id: id }, - data: { - ...updateAdminProfileDto, - }, - }); - - console.log('Updated admin:', updatedAdmin); - - return new ProfileDto({ ...updatedAdmin }); - } - - async findAll(accountId: string): Promise { - const admins = await this.prisma.user.findMany({ - where: { - deletedAt: null, - OR: [ - { - AccountUser: { - some: { - accountId: accountId, - Role: { - name: 'Admin', - }, - }, - }, - }, - { - AccountUser: { - some: { - accountId: accountId, - Role: { - name: 'Owner', - }, - }, - }, - }, - ], - }, - include: { - AccountUser: { - include: { - Role: true, - }, - }, - }, - }); - - console.log('Filtered Admin Data:', JSON.stringify(admins, null, 2)); - - return admins.map((admin) => { - const accountUser = admin.AccountUser.find( - (au) => au.accountId === accountId, - ); - - return new ProfileDto({ - id: admin.id, - name: admin.name, - email: admin.email, - isActive: accountUser?.isActive, - }); - }); - } - - async toggleActiveStatus( - accountId: string, - userId: string, - ): Promise { - const accountUser = await this.prisma.accountUser.findFirst({ - where: { - accountId: accountId, - userId: userId, - }, - }); - - const updatedAccountUser = await this.prisma.accountUser.update({ - where: { - id: accountUser.id, - }, - data: { - isActive: !accountUser.isActive, - }, - }); - - const updatedUser = await this.prisma.user.findFirst({ - where: { - id: userId, - deletedAt: null, - }, - include: { - AccountUser: { - where: { - accountId: accountId, - }, - }, - }, - }); - - if (!updatedUser) { - throw new NotFoundException('User not found'); - } - - return new ProfileDto({ - id: updatedUser.id, - name: updatedUser.name, - email: updatedUser.email, - isActive: updatedAccountUser.isActive, - }); - } -} From 04a939d848d1631e4c0b202ddcbe0a49990ae526 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Tue, 1 Apr 2025 16:55:02 +0300 Subject: [PATCH 30/43] added the owner to returned admins list --- src/modules/admin/user/user.service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/modules/admin/user/user.service.ts b/src/modules/admin/user/user.service.ts index a975f986..1bd94c6f 100644 --- a/src/modules/admin/user/user.service.ts +++ b/src/modules/admin/user/user.service.ts @@ -91,7 +91,10 @@ export class UserService { AccountUser: { some: { accountId: accountId, - ...(roleId ? { roleId: roleId } : {}), + OR: [ + { Role: { name: 'Owner' } }, + ...(roleId ? [{ roleId: roleId }] : []), + ], }, }, }; From ec8f1b5c16e68adb306434a85cb69ea28939451f Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Tue, 1 Apr 2025 16:57:53 +0300 Subject: [PATCH 31/43] change the some to filter on active acccounts --- src/modules/auth/guard/role/role.guard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/auth/guard/role/role.guard.ts b/src/modules/auth/guard/role/role.guard.ts index 3cf8b57f..6bd095b6 100644 --- a/src/modules/auth/guard/role/role.guard.ts +++ b/src/modules/auth/guard/role/role.guard.ts @@ -37,7 +37,7 @@ export class RoleGuard implements CanActivate { const activeAccountIds = activeAccounts.map((account) => account.accountId); - const hasActiveAccount = user.accounts.some((account) => + const hasActiveAccount = user.accounts.filter((account) => activeAccountIds.includes(account.id), ); From 3324d522fbd76fbcb4291e35cb7c6e340a3f67fd Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Wed, 2 Apr 2025 15:45:58 +0300 Subject: [PATCH 32/43] fixed pr-comments --- src/modules/admin/role/role.controller.ts | 8 +- src/modules/admin/role/role.service.ts | 5 +- src/modules/admin/user/dto/create-user.dto.ts | 3 +- src/modules/admin/user/dto/user.dto.ts | 4 + src/modules/admin/user/user.service.ts | 78 ++++++++++++++----- src/modules/auth/auth.service.ts | 2 +- src/modules/auth/guard/role/role.guard.ts | 62 +++------------ 7 files changed, 84 insertions(+), 78 deletions(-) diff --git a/src/modules/admin/role/role.controller.ts b/src/modules/admin/role/role.controller.ts index 4e5f78a0..041718bc 100644 --- a/src/modules/admin/role/role.controller.ts +++ b/src/modules/admin/role/role.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Param, UseGuards } from '@nestjs/common'; +import { Controller, Get, UseGuards } from '@nestjs/common'; import { RoleGuard } from '../../auth/guard/role/role.guard'; import { Roles } from '../../auth/auth.decorator'; import { RoleService } from './role.service'; @@ -9,10 +9,10 @@ import { AuthGuard } from 'src/modules/auth/guard/auth/auth.guard'; export class RolesController { constructor(private readonly roleService: RoleService) {} - @Get(':accountId') + @Get('') @UseGuards(RoleGuard) @Roles('OWNER', 'ADMIN') - findAll(@Param('accountId') accountId) { - return this.roleService.findAll(accountId); + findAll() { + return this.roleService.findAll(); } } diff --git a/src/modules/admin/role/role.service.ts b/src/modules/admin/role/role.service.ts index 3a3c801d..2100ad53 100644 --- a/src/modules/admin/role/role.service.ts +++ b/src/modules/admin/role/role.service.ts @@ -6,11 +6,8 @@ import { RoleDto } from './dto/role.dto'; export class RoleService { constructor(private readonly prisma: PrismaService) {} - async findAll(accountId: string): Promise { + async findAll(): Promise { const roles = await this.prisma.role.findMany({ - where: { - accountId: accountId, - }, select: { id: true, name: true, diff --git a/src/modules/admin/user/dto/create-user.dto.ts b/src/modules/admin/user/dto/create-user.dto.ts index 9850a294..9cd6c737 100644 --- a/src/modules/admin/user/dto/create-user.dto.ts +++ b/src/modules/admin/user/dto/create-user.dto.ts @@ -1,4 +1,4 @@ -import { IsEmail, IsString } from 'class-validator'; +import { IsEmail, IsOptional, IsString } from 'class-validator'; export class CreateUserDto { @IsString() @@ -10,5 +10,6 @@ export class CreateUserDto { @IsEmail() email: string; @IsString() + @IsOptional() password: string; } diff --git a/src/modules/admin/user/dto/user.dto.ts b/src/modules/admin/user/dto/user.dto.ts index 88136458..3d884d1d 100644 --- a/src/modules/admin/user/dto/user.dto.ts +++ b/src/modules/admin/user/dto/user.dto.ts @@ -16,6 +16,9 @@ export class UserDto { @Expose() role?: string; + @Expose() + status?: boolean; + @Expose() createdAt: Date; @@ -28,6 +31,7 @@ export class UserDto { this.email = partial.email; this.imageUrl = partial.imageUrl; this.role = partial.role; + this.status = partial.status; this.createdAt = partial.createdAt; this.updatedAt = partial.updatedAt; } diff --git a/src/modules/admin/user/user.service.ts b/src/modules/admin/user/user.service.ts index 1bd94c6f..70180067 100644 --- a/src/modules/admin/user/user.service.ts +++ b/src/modules/admin/user/user.service.ts @@ -1,11 +1,14 @@ -import { Inject, ForbiddenException } from '@nestjs/common'; +import { + Inject, + ForbiddenException, + HttpException, + HttpStatus, +} from '@nestjs/common'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { PrismaService } from 'src/modules/prisma/prisma.service'; import { UserDto } from './dto/user.dto'; import { REQUEST } from '@nestjs/core'; -import * as bcrypt from 'bcryptjs'; -import { AuthService } from 'src/modules/auth/auth.service'; import { paginate } from 'src/common/helpers/pagination'; import { User, AccountUser, Role } from '@prisma/client'; // Assuming User is from Prisma model@Injectable() import { GetAllUsersQueryDto } from './dto/get-all-users-query.dto'; @@ -14,6 +17,7 @@ export class UserService { @Inject(REQUEST) private readonly request: any, private prisma: PrismaService, ) {} + async create(createUserDto: CreateUserDto) { const account = await this.prisma.account.findUnique({ where: { id: createUserDto.accountId }, @@ -25,24 +29,54 @@ export class UserService { await this.validateAccountAccess(createUserDto.accountId); + const role = await this.prisma.role.findFirst({ + where: { + id: createUserDto.roleId, + }, + }); + + if (role?.name === 'Owner') { + throw new HttpException( + 'You cannot create a user with owner role!', + HttpStatus.FORBIDDEN, + ); + } + const existingUser = await this.prisma.user.findUnique({ where: { email: createUserDto.email }, }); if (existingUser) { - throw new Error('Email already in use'); - } + const existingRelation = await this.prisma.accountUser.findFirst({ + where: { + userId: existingUser.id, + accountId: createUserDto.accountId, + }, + }); + + if (existingRelation) { + throw new HttpException( + 'The user already exists in your organization!', + HttpStatus.CONFLICT, + ); + } + + await this.prisma.accountUser.create({ + data: { + userId: existingUser.id, + accountId: createUserDto.accountId, + roleId: createUserDto.roleId, + }, + }); - const hashedPassword = await bcrypt.hash( - createUserDto.password, - AuthService.saltRounds, - ); + return { message: 'User added to the organization successfully!' }; // Stop here + } const userData = await this.prisma.user.create({ data: { name: createUserDto.name, email: createUserDto.email, - password: hashedPassword, + password: '', AccountUser: { create: { accountId: createUserDto.accountId, @@ -89,13 +123,16 @@ export class UserService { const whereCondition = { AccountUser: { - some: { - accountId: accountId, - OR: [ - { Role: { name: 'Owner' } }, - ...(roleId ? [{ roleId: roleId }] : []), - ], - }, + some: roleId + ? { + accountId: accountId, + deletedAt: null, + OR: [{ Role: { name: 'Owner' } }, { roleId: roleId }], + } + : { + accountId: accountId, + deletedAt: null, + }, }, }; @@ -123,12 +160,15 @@ export class UserService { ? user.AccountUser[0].Role.name : ''; + const isActive = user.AccountUser[0].isActive; + return new UserDto({ id: user.id, name: user.name, email: user.email, imageUrl: user.imageUrl, role: role, + status: isActive, createdAt: user.createdAt, updatedAt: user.updatedAt, }); @@ -169,6 +209,7 @@ export class UserService { include: { AccountUser: true }, }); + if (!userToDelete) { throw new Error('User not found or you do not have access.'); } @@ -176,10 +217,11 @@ export class UserService { const updatedUser = await this.prisma.user.update({ where: { id }, data: { + deletedAt: new Date(), AccountUser: { updateMany: { where: { accountId }, - data: { deletedAt: Date() }, + data: { deletedAt: new Date() }, }, }, }, diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 4c8f208c..6192aceb 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -129,7 +129,7 @@ export class AuthService { async getUserAccounts(userId: string) { const accounts = await this.prisma.account.findMany({ - where: { AccountUser: { some: { userId } } }, + where: { AccountUser: { some: { userId, isActive: true } } }, select: { id: true, name: true, diff --git a/src/modules/auth/guard/role/role.guard.ts b/src/modules/auth/guard/role/role.guard.ts index 6bd095b6..6bc5cb7b 100644 --- a/src/modules/auth/guard/role/role.guard.ts +++ b/src/modules/auth/guard/role/role.guard.ts @@ -1,64 +1,26 @@ -import { - CanActivate, - ExecutionContext, - Injectable, - ForbiddenException, - NotFoundException, -} from '@nestjs/common'; +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; +import { Observable } from 'rxjs'; import { ROLE_KEY } from '../../auth.decorator'; -import { PrismaService } from 'src/modules/prisma/prisma.service'; -import { User } from 'src/modules/admin/user/entities/user.entity'; @Injectable() export class RoleGuard implements CanActivate { - constructor( - private reflector: Reflector, - private prisma: PrismaService, - ) {} - - async canActivate(context: ExecutionContext): Promise { + constructor(private reflector: Reflector) {} + canActivate( + context: ExecutionContext, + ): boolean | Promise | Observable { const requiredRoles = this.reflector.getAllAndOverride(ROLE_KEY, [ context.getHandler(), context.getClass(), ]); + console.log(requiredRoles); - const user: User = context.switchToHttp().getRequest().user; - - const activeAccounts = await this.prisma.accountUser.findMany({ - where: { - userId: user.id, - isActive: true, - }, - select: { - accountId: true, - }, - }); - - const activeAccountIds = activeAccounts.map((account) => account.accountId); - - const hasActiveAccount = user.accounts.filter((account) => - activeAccountIds.includes(account.id), - ); - - if (!hasActiveAccount) { - throw new ForbiddenException( - "Your organization's owner has disabled your account", - ); - } + const user = context.switchToHttp().getRequest().user; - const hasRequiredRole = user.accounts.some( - (account) => - activeAccountIds.includes(account.id) && - requiredRoles.some( - (role) => account.role?.name.toLowerCase() === role.toLowerCase(), - ), + return user.accounts?.some((account) => + requiredRoles.some( + (role) => account.role?.name.toLowerCase() === role.toLowerCase(), + ), ); - - if (!hasRequiredRole) { - throw new NotFoundException('Forbidden resource'); - } - - return true; } } From 9700cfb8a978e926f35fff6119dd42dda6c88fd4 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Wed, 2 Apr 2025 15:46:31 +0300 Subject: [PATCH 33/43] fixed es lint error --- src/modules/admin/user/user.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/admin/user/user.service.ts b/src/modules/admin/user/user.service.ts index 70180067..191a18ee 100644 --- a/src/modules/admin/user/user.service.ts +++ b/src/modules/admin/user/user.service.ts @@ -209,7 +209,6 @@ export class UserService { include: { AccountUser: true }, }); - if (!userToDelete) { throw new Error('User not found or you do not have access.'); } From 73fa9c84beaa3cc1f1f38629a15b5e8a756fde4b Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Wed, 2 Apr 2025 16:00:53 +0300 Subject: [PATCH 34/43] dis-allwoed the owner from deactivating it's own account --- src/modules/admin/user/user.service.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/modules/admin/user/user.service.ts b/src/modules/admin/user/user.service.ts index 191a18ee..5f102608 100644 --- a/src/modules/admin/user/user.service.ts +++ b/src/modules/admin/user/user.service.ts @@ -251,8 +251,18 @@ export class UserService { accountId: accountId, userId: userId, }, + include: { + Role: true, + }, }); + if (accountUser.Role.name === 'Owner') { + throw new HttpException( + 'You can not deactivate your own account!', + HttpStatus.CONFLICT, + ); + } + const updatedStatus = await this.prisma.accountUser.update({ where: { id: accountUser.id, From 796fcf3da0c372915d5ce1bf64e857b037d0ef6a Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Fri, 4 Apr 2025 07:36:03 +0300 Subject: [PATCH 35/43] added isActive to get all users endpoint --- .../admin/user/dto/get-all-users-query.dto.ts | 13 +++++++++++-- src/modules/admin/user/user.service.ts | 5 ++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/modules/admin/user/dto/get-all-users-query.dto.ts b/src/modules/admin/user/dto/get-all-users-query.dto.ts index 22e66509..5de7dc5e 100644 --- a/src/modules/admin/user/dto/get-all-users-query.dto.ts +++ b/src/modules/admin/user/dto/get-all-users-query.dto.ts @@ -1,5 +1,5 @@ -import { IsInt, IsOptional, Min, IsString } from 'class-validator'; -import { Type } from 'class-transformer'; +import { IsInt, IsOptional, Min, IsString, IsBoolean } from 'class-validator'; +import { Transform, Type } from 'class-transformer'; export class GetAllUsersQueryDto { @IsOptional() @@ -10,6 +10,15 @@ export class GetAllUsersQueryDto { @IsString() roleId?: string; + @IsOptional() + @IsBoolean() + @Transform(({ value }) => { + if (value === 'true') return true; + if (value === 'false') return false; + return value; + }) + isActive?: boolean; + @IsOptional() @Type(() => Number) @IsInt() diff --git a/src/modules/admin/user/user.service.ts b/src/modules/admin/user/user.service.ts index 5f102608..2625bbaf 100644 --- a/src/modules/admin/user/user.service.ts +++ b/src/modules/admin/user/user.service.ts @@ -117,7 +117,8 @@ export class UserService { } async findAllUsers(query: GetAllUsersQueryDto) { - const { accountId, roleId, page, limit } = query; + const { accountId, roleId, page, limit, isActive } = query; + console.log('Is active value', isActive); await this.validateAccountAccess(accountId); @@ -126,11 +127,13 @@ export class UserService { some: roleId ? { accountId: accountId, + isActive: isActive !== undefined ? isActive : undefined, deletedAt: null, OR: [{ Role: { name: 'Owner' } }, { roleId: roleId }], } : { accountId: accountId, + isActive: isActive !== undefined ? isActive : undefined, deletedAt: null, }, }, From 6f9e12c8317a7098c31675c23361d01e8244dd38 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Mon, 7 Apr 2025 14:14:04 +0300 Subject: [PATCH 36/43] added pagination and filter by accountID to the get all conversation endpoint --- .../conversation/conversation.controller.ts | 8 ++- .../conversation/conversation.service.ts | 56 ++++++++++++++++--- .../conversation/dto/get-conversation.dto.ts | 7 +++ 3 files changed, 60 insertions(+), 11 deletions(-) create mode 100644 src/modules/mentor/conversation/dto/get-conversation.dto.ts diff --git a/src/modules/mentor/conversation/conversation.controller.ts b/src/modules/mentor/conversation/conversation.controller.ts index be740c00..a7b196d4 100644 --- a/src/modules/mentor/conversation/conversation.controller.ts +++ b/src/modules/mentor/conversation/conversation.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Body, Patch, Param, Delete } from '@nestjs/common'; +import { Controller, Get, Body, Patch, Param, Delete, ValidationPipe, Query } from '@nestjs/common'; import { ConversationService } from './conversation.service'; import { UpdateConversationDto } from './dto/update-conversation.dto'; @@ -7,8 +7,10 @@ export class ConversationController { constructor(private readonly conversationService: ConversationService) {} @Get() - findAll() { - return this.conversationService.findAll(); + findAll( + @Query(new ValidationPipe({ transform: true })) query: Record, + ) { + return this.conversationService.findAll(query); } @Get(':id') diff --git a/src/modules/mentor/conversation/conversation.service.ts b/src/modules/mentor/conversation/conversation.service.ts index 6d15911a..6fa0adac 100644 --- a/src/modules/mentor/conversation/conversation.service.ts +++ b/src/modules/mentor/conversation/conversation.service.ts @@ -1,22 +1,62 @@ import { Injectable } from '@nestjs/common'; import { UpdateConversationDto } from './dto/update-conversation.dto'; import { PrismaService } from 'src/modules/prisma/prisma.service'; +import { channel } from 'diagnostics_channel'; +import { PaginationDto } from 'src/common/dto/pagination.dto'; +import { paginate, PaginationResult } from 'src/common/helpers/pagination'; +import { GetConversationDto } from './dto/get-conversation.dto'; @Injectable() export class ConversationService { constructor(private readonly prisma: PrismaService) {} - async findAll() { - const conversations = await this.prisma.conversation.findMany({ - include: { - Channel: true, + async findAll( + query: Record, + ){ + const getConversationDto = new GetConversationDto(); + getConversationDto.accountId = query.accountId; + + const paginationDto = new PaginationDto(); + paginationDto.page = query.page ? parseInt(query.page) : 1; + paginationDto.limit = query.limit ? parseInt(query.limit) : 10; + + const result = await paginate( + this.prisma, + this.prisma.conversation, + { + Channel: { + accountId: getConversationDto.accountId, + }, }, - }); + paginationDto.page, + paginationDto.limit, + { + Channel: { + select: { + name: true, + type: true, + accountId: true, + }, + }, + Mentor: { + select: { + name: true, + }, + }, + }, + ); - return conversations.map((conversation) => ({ - conversation_id: conversation.id, - platform: conversation.Channel.type, + const transformedData = (result.data as Array).map((conversation) => ({ + mentorName: conversation.Mentor?.name, + conversationId: conversation.id, + platform: conversation.Channel?.type, + channelName: conversation.Channel?.name })); + + return { + ...result, + data: transformedData, + }; } async findOne(id: string) { diff --git a/src/modules/mentor/conversation/dto/get-conversation.dto.ts b/src/modules/mentor/conversation/dto/get-conversation.dto.ts new file mode 100644 index 00000000..b19d31b7 --- /dev/null +++ b/src/modules/mentor/conversation/dto/get-conversation.dto.ts @@ -0,0 +1,7 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class GetConversationDto { + @IsNotEmpty() + @IsString() + accountId: string; +} From e9e7af6b19c9a748d8207cdcd5e271de0e30ae23 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Mon, 7 Apr 2025 15:20:36 +0300 Subject: [PATCH 37/43] added change conversation mentor endpoint --- .../conversation/conversation.controller.ts | 19 ++++++++++++++- .../conversation/conversation.service.ts | 23 +++++++++++++++---- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/modules/mentor/conversation/conversation.controller.ts b/src/modules/mentor/conversation/conversation.controller.ts index a7b196d4..fe684071 100644 --- a/src/modules/mentor/conversation/conversation.controller.ts +++ b/src/modules/mentor/conversation/conversation.controller.ts @@ -1,4 +1,13 @@ -import { Controller, Get, Body, Patch, Param, Delete, ValidationPipe, Query } from '@nestjs/common'; +import { + Controller, + Get, + Body, + Patch, + Param, + Delete, + ValidationPipe, + Query, +} from '@nestjs/common'; import { ConversationService } from './conversation.service'; import { UpdateConversationDto } from './dto/update-conversation.dto'; @@ -30,4 +39,12 @@ export class ConversationController { remove(@Param('id') id: string) { return this.conversationService.remove(id); } + + @Patch(':conversationId/:mentorId') + changeMentor( + @Param('conversationId') conversationId: string, + @Param('mentorId') mentorId: string, + ) { + return this.conversationService.changeMentor(conversationId, mentorId); + } } diff --git a/src/modules/mentor/conversation/conversation.service.ts b/src/modules/mentor/conversation/conversation.service.ts index 6fa0adac..17f593fd 100644 --- a/src/modules/mentor/conversation/conversation.service.ts +++ b/src/modules/mentor/conversation/conversation.service.ts @@ -10,9 +10,7 @@ import { GetConversationDto } from './dto/get-conversation.dto'; export class ConversationService { constructor(private readonly prisma: PrismaService) {} - async findAll( - query: Record, - ){ + async findAll(query: Record) { const getConversationDto = new GetConversationDto(); getConversationDto.accountId = query.accountId; @@ -50,7 +48,7 @@ export class ConversationService { mentorName: conversation.Mentor?.name, conversationId: conversation.id, platform: conversation.Channel?.type, - channelName: conversation.Channel?.name + channelName: conversation.Channel?.name, })); return { @@ -96,4 +94,21 @@ export class ConversationService { remove(id: string) { return this.prisma.conversation.delete({ where: { id: id } }); } + + async changeMentor(conversationId: string, mentorId: string) { + const conversation = await this.prisma.conversation.update({ + where: { id: conversationId }, + data: { + mentorId: mentorId, + }, + include: { + Mentor: true, + }, + }); + + return { + mentorName: conversation.Mentor.name, + message: 'Mentor updated successfully!', + }; + } } From e9f0d17322273742eeac4123c7238ffd454b0ea5 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Fri, 25 Apr 2025 11:46:56 +0300 Subject: [PATCH 38/43] fixed es-lint error --- src/modules/mentor/conversation/conversation.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/modules/mentor/conversation/conversation.service.ts b/src/modules/mentor/conversation/conversation.service.ts index 17f593fd..05cc54b5 100644 --- a/src/modules/mentor/conversation/conversation.service.ts +++ b/src/modules/mentor/conversation/conversation.service.ts @@ -1,9 +1,8 @@ import { Injectable } from '@nestjs/common'; import { UpdateConversationDto } from './dto/update-conversation.dto'; import { PrismaService } from 'src/modules/prisma/prisma.service'; -import { channel } from 'diagnostics_channel'; import { PaginationDto } from 'src/common/dto/pagination.dto'; -import { paginate, PaginationResult } from 'src/common/helpers/pagination'; +import { paginate } from 'src/common/helpers/pagination'; import { GetConversationDto } from './dto/get-conversation.dto'; @Injectable() From 6033b46876c60a7efade82e063d35969e94cf69f Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Thu, 22 May 2025 11:22:00 +0300 Subject: [PATCH 39/43] added admin/dashboard/accountId endpoint to get all datas needed for dashboard in one place --- src/modules/admin/admin.module.ts | 2 + .../dashboard/dashboard.controller.spec.ts | 20 +++ .../admin/dashboard/dashboard.controller.ts | 28 ++++ .../admin/dashboard/dashboard.module.ts | 13 ++ .../admin/dashboard/dashboard.service.spec.ts | 18 +++ .../admin/dashboard/dashboard.service.ts | 122 ++++++++++++++++++ src/types/mentorExpertise.ts | 4 + 7 files changed, 207 insertions(+) create mode 100644 src/modules/admin/dashboard/dashboard.controller.spec.ts create mode 100644 src/modules/admin/dashboard/dashboard.controller.ts create mode 100644 src/modules/admin/dashboard/dashboard.module.ts create mode 100644 src/modules/admin/dashboard/dashboard.service.spec.ts create mode 100644 src/modules/admin/dashboard/dashboard.service.ts create mode 100644 src/types/mentorExpertise.ts diff --git a/src/modules/admin/admin.module.ts b/src/modules/admin/admin.module.ts index 42fdfea5..4e6e3e97 100644 --- a/src/modules/admin/admin.module.ts +++ b/src/modules/admin/admin.module.ts @@ -4,6 +4,7 @@ import { UserModule } from './user/user.module'; import { AccountModule } from './account/account.module'; import { MentorModule } from './mentor/mentor.module'; import { ChannelModule } from './channel/channel.module'; +import { DashboardModule } from './dashboard/dashboard.module'; @Module({ imports: [ @@ -13,6 +14,7 @@ import { ChannelModule } from './channel/channel.module'; MentorModule, ChannelModule, MessageModule, + DashboardModule, ], }) export class AdminModule {} diff --git a/src/modules/admin/dashboard/dashboard.controller.spec.ts b/src/modules/admin/dashboard/dashboard.controller.spec.ts new file mode 100644 index 00000000..9ae8da71 --- /dev/null +++ b/src/modules/admin/dashboard/dashboard.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DashboardController } from './dashboard.controller'; +import { DashboardService } from './dashboard.service'; + +describe('DashboardController', () => { + let controller: DashboardController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [DashboardController], + providers: [DashboardService], + }).compile(); + + controller = module.get(DashboardController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/modules/admin/dashboard/dashboard.controller.ts b/src/modules/admin/dashboard/dashboard.controller.ts new file mode 100644 index 00000000..320c7024 --- /dev/null +++ b/src/modules/admin/dashboard/dashboard.controller.ts @@ -0,0 +1,28 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + UseGuards, +} from '@nestjs/common'; +import { DashboardService } from './dashboard.service'; +import { CreateDashboardDto } from './dto/create-dashboard.dto'; +import { UpdateDashboardDto } from './dto/update-dashboard.dto'; +import { AuthGuard } from 'src/modules/auth/guard/auth/auth.guard'; +import { RoleGuard } from 'src/modules/auth/guard/role/role.guard'; +import { Roles } from 'src/modules/auth/auth.decorator'; + +@Controller('admin/dashboard') +@UseGuards(AuthGuard, RoleGuard) +@Roles('OWNER', 'ADMIN') +export class DashboardController { + constructor(private readonly dashboardService: DashboardService) {} + + @Get(':accountId') + findAll(@Param('accountId') accountId: string) { + return this.dashboardService.getDashboardStats(accountId); + } +} diff --git a/src/modules/admin/dashboard/dashboard.module.ts b/src/modules/admin/dashboard/dashboard.module.ts new file mode 100644 index 00000000..69955b2b --- /dev/null +++ b/src/modules/admin/dashboard/dashboard.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { DashboardService } from './dashboard.service'; +import { DashboardController } from './dashboard.controller'; +import { PrismaModule } from 'src/modules/prisma/prisma.module'; +import { PrismaService } from 'src/modules/prisma/prisma.service'; +import { JwtModule } from '@nestjs/jwt'; + +@Module({ + imports: [PrismaModule, JwtModule], + controllers: [DashboardController], + providers: [DashboardService, PrismaService], +}) +export class DashboardModule {} diff --git a/src/modules/admin/dashboard/dashboard.service.spec.ts b/src/modules/admin/dashboard/dashboard.service.spec.ts new file mode 100644 index 00000000..dc3993ae --- /dev/null +++ b/src/modules/admin/dashboard/dashboard.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DashboardService } from './dashboard.service'; + +describe('DashboardService', () => { + let service: DashboardService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [DashboardService], + }).compile(); + + service = module.get(DashboardService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/modules/admin/dashboard/dashboard.service.ts b/src/modules/admin/dashboard/dashboard.service.ts new file mode 100644 index 00000000..36a0857d --- /dev/null +++ b/src/modules/admin/dashboard/dashboard.service.ts @@ -0,0 +1,122 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from 'src/modules/prisma/prisma.service'; +import { MentorExpertiseRow } from 'src/types/mentorExpertise'; + +@Injectable() +export class DashboardService { + constructor(private readonly prisma: PrismaService) {} + + async getDashboardStats(accountId) { + const totalUsers = await this.prisma.accountUser.count({ + where: { + deletedAt: null, + accountId: accountId, + }, + }); + + const totalMentors = await this.prisma.accountUser.count({ + where: { + deletedAt: null, + accountId: accountId, + Role: { + name: 'Mentor', + }, + }, + }); + + const activeMentors = await this.prisma.accountUser.count({ + where: { + deletedAt: null, + isActive: true, + accountId: accountId, + Role: { + name: 'Mentor', + }, + }, + }); + + const activeUsers = await this.prisma.accountUser.count({ + where: { + deletedAt: null, + isActive: true, + accountId: accountId, + }, + }); + + const totalMentees = await this.prisma.conversation.findMany({ + where: { + Channel: { + accountId: accountId, + }, + }, + distinct: ['address'], + select: { + address: true, + }, + }); + + const sixMonthsAgo = new Date(); + sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6); + + const users = await this.prisma.accountUser.findMany({ + where: { + createdAt: { + gte: sixMonthsAgo, + }, + deletedAt: null, + accountId, + }, + select: { + createdAt: true, + }, + }); + + const growthMap: Record = {}; + + for (const { createdAt } of users) { + const date = createdAt.toISOString().split('T')[0]; + growthMap[date] = (growthMap[date] || 0) + 1; + } + + const groupedByDay = Object.entries(growthMap).map(([date, count]) => ({ + createdAt: date, + _count: { id: count }, + })); + + const mentor_by_expertise = await this.prisma.$queryRawUnsafe< + MentorExpertiseRow[] + >( + ` + SELECT + expertise_item AS expertise, + COUNT(DISTINCT mentor_id) AS count + FROM ( + SELECT + id AS mentor_id, + jsonb_array_elements_text(expertise) AS expertise_item + FROM "Mentor" + WHERE "accountId" = $1 + ) AS expanded + GROUP BY expertise_item + ORDER BY count DESC + `, + accountId, + ); + + return { + totalUsers, + totalMentors, + activeMentors, + activeUsers, + totalMentees: totalMentees.length, + mentorByExpertise: mentor_by_expertise.map((item) => ({ + expertise: item.expertise, + count: Number(item.count), + })), + userGrowth: groupedByDay.map((item) => ({ + date: item.createdAt, + count: item._count.id, + })), + }; + } +} diff --git a/src/types/mentorExpertise.ts b/src/types/mentorExpertise.ts new file mode 100644 index 00000000..3b7f52b9 --- /dev/null +++ b/src/types/mentorExpertise.ts @@ -0,0 +1,4 @@ +export interface MentorExpertiseRow { + expertise: string; + count: bigint; +} From b5ed8187294b8e6edd81104a45477c8265a9d967 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Thu, 22 May 2025 11:22:30 +0300 Subject: [PATCH 40/43] removed un used import --- src/modules/admin/dashboard/dashboard.controller.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/modules/admin/dashboard/dashboard.controller.ts b/src/modules/admin/dashboard/dashboard.controller.ts index 320c7024..4e278ce8 100644 --- a/src/modules/admin/dashboard/dashboard.controller.ts +++ b/src/modules/admin/dashboard/dashboard.controller.ts @@ -9,8 +9,6 @@ import { UseGuards, } from '@nestjs/common'; import { DashboardService } from './dashboard.service'; -import { CreateDashboardDto } from './dto/create-dashboard.dto'; -import { UpdateDashboardDto } from './dto/update-dashboard.dto'; import { AuthGuard } from 'src/modules/auth/guard/auth/auth.guard'; import { RoleGuard } from 'src/modules/auth/guard/role/role.guard'; import { Roles } from 'src/modules/auth/auth.decorator'; From 1c9eca5950f198e9aeec20f808ba64b85ae4816b Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Thu, 22 May 2025 11:25:16 +0300 Subject: [PATCH 41/43] fixed es lint error --- src/modules/admin/dashboard/dashboard.controller.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/modules/admin/dashboard/dashboard.controller.ts b/src/modules/admin/dashboard/dashboard.controller.ts index 4e278ce8..f4e53dfe 100644 --- a/src/modules/admin/dashboard/dashboard.controller.ts +++ b/src/modules/admin/dashboard/dashboard.controller.ts @@ -1,13 +1,4 @@ -import { - Controller, - Get, - Post, - Body, - Patch, - Param, - Delete, - UseGuards, -} from '@nestjs/common'; +import { Controller, Get, Param, UseGuards } from '@nestjs/common'; import { DashboardService } from './dashboard.service'; import { AuthGuard } from 'src/modules/auth/guard/auth/auth.guard'; import { RoleGuard } from 'src/modules/auth/guard/role/role.guard'; From 83f9169faf34a04feffdbbede5cfeadb8fa0266e Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Thu, 22 May 2025 11:58:09 +0300 Subject: [PATCH 42/43] update the user growth to return full year and in months --- .../admin/dashboard/dashboard.service.ts | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/src/modules/admin/dashboard/dashboard.service.ts b/src/modules/admin/dashboard/dashboard.service.ts index 36a0857d..c2fd6588 100644 --- a/src/modules/admin/dashboard/dashboard.service.ts +++ b/src/modules/admin/dashboard/dashboard.service.ts @@ -55,13 +55,16 @@ export class DashboardService { }, }); - const sixMonthsAgo = new Date(); - sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6); + const currentYear = new Date().getFullYear(); + + const startOfYear = new Date(currentYear, 0, 1); + const startOfNextYear = new Date(currentYear + 1, 0, 1); const users = await this.prisma.accountUser.findMany({ where: { createdAt: { - gte: sixMonthsAgo, + gte: startOfYear, + lt: startOfNextYear, }, deletedAt: null, accountId, @@ -73,15 +76,28 @@ export class DashboardService { const growthMap: Record = {}; + const monthNames = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ]; + for (const { createdAt } of users) { - const date = createdAt.toISOString().split('T')[0]; - growthMap[date] = (growthMap[date] || 0) + 1; - } + const year = createdAt.getFullYear(); + const month = monthNames[createdAt.getMonth()]; + const monthYear = `${month} ${year}`; // e.g. "May 2025" - const groupedByDay = Object.entries(growthMap).map(([date, count]) => ({ - createdAt: date, - _count: { id: count }, - })); + growthMap[monthYear] = (growthMap[monthYear] || 0) + 1; + } const mentor_by_expertise = await this.prisma.$queryRawUnsafe< MentorExpertiseRow[] @@ -113,10 +129,10 @@ export class DashboardService { expertise: item.expertise, count: Number(item.count), })), - userGrowth: groupedByDay.map((item) => ({ - date: item.createdAt, - count: item._count.id, - })), + userGrowth: Object.entries(growthMap).map(([monthYear, count]) => ({ + month: monthYear, + count, + })) }; } } From d04333c999ba7c5c3de974558f4f12df490505f2 Mon Sep 17 00:00:00 2001 From: bikilaketema Date: Thu, 22 May 2025 11:59:48 +0300 Subject: [PATCH 43/43] fixed es lint error --- src/modules/admin/dashboard/dashboard.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/admin/dashboard/dashboard.service.ts b/src/modules/admin/dashboard/dashboard.service.ts index c2fd6588..5282ab9c 100644 --- a/src/modules/admin/dashboard/dashboard.service.ts +++ b/src/modules/admin/dashboard/dashboard.service.ts @@ -132,7 +132,7 @@ export class DashboardService { userGrowth: Object.entries(growthMap).map(([monthYear, count]) => ({ month: monthYear, count, - })) + })), }; } }