diff --git a/package.json b/package.json index 4f8508d..4085744 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "scripts": { "db:create": "npx mikro-orm schema:create --run --config ./src/config/mikro-orm.config.dev.ts", "db:update": "npx mikro-orm schema:update --run --config ./src/config/mikro-orm.config.dev.ts", + "db:drop": "npx mikro-orm schema:drop --run --config ./src/config/mikro-orm.config.dev.ts", "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start dist/main", diff --git a/src/modules/admin/admin.controller.ts b/src/modules/admin/admin.controller.ts new file mode 100644 index 0000000..de7e9a0 --- /dev/null +++ b/src/modules/admin/admin.controller.ts @@ -0,0 +1,26 @@ +import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common'; +import { AdminService } from './admin.service'; +import { CreateAdminDto } from './dto/create-admin.dto'; +import { ApiTags, ApiOperation, ApiBody, ApiResponse } from '@nestjs/swagger'; + +@ApiTags('admin') +@Controller('admin') +export class AdminController { + constructor(private readonly adminService: AdminService) {} + + @Post() + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ summary: 'Create a new admin' }) + @ApiBody({ type: CreateAdminDto }) + @ApiResponse({ status: 201, description: 'Admin created' }) + async create(@Body() dto: CreateAdminDto) { + const admin = await this.adminService.create({ + phone: dto.phone, + firstName: dto.firstName, + lastName: dto.lastName, + roleId: dto.roleId, + restId: dto.restId, + }); + return admin; + } +} diff --git a/src/modules/admin/admin.module.ts b/src/modules/admin/admin.module.ts index f0425a8..9c48732 100644 --- a/src/modules/admin/admin.module.ts +++ b/src/modules/admin/admin.module.ts @@ -7,11 +7,14 @@ import { AdminRepository } from './repositories/rest.repository'; import { Role } from './entities/role.entity'; import { Permission } from './entities/permission.entity'; import { RolePermission } from './entities/rolePermission.entity'; +import { AdminController } from './admin.controller'; +import { RoleService } from './providers/role.service'; +import { RoleController } from './controllers/role.controller'; @Module({ - providers: [AdminService, AdminRepository], - controllers: [], + providers: [AdminService, AdminRepository, RoleService], + controllers: [AdminController, RoleController], imports: [MikroOrmModule.forFeature([Admin, Role, Permission, RolePermission]), JwtModule], - exports: [AdminService, AdminRepository], + exports: [AdminService, AdminRepository, RoleService], }) export class AdminModule {} diff --git a/src/modules/admin/admin.service.ts b/src/modules/admin/admin.service.ts index 56280c4..63820fe 100644 --- a/src/modules/admin/admin.service.ts +++ b/src/modules/admin/admin.service.ts @@ -2,6 +2,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@mikro-orm/nestjs'; import { EntityRepository } from '@mikro-orm/core'; import { Admin } from './entities/admin.entity'; +import { Role } from './entities/role.entity'; +import { Restaurant } from '../restaurants/entities/restaurant.entity'; import { EntityManager } from '@mikro-orm/postgresql'; @Injectable() @@ -20,9 +22,24 @@ export class AdminService { return this.adminRepository.findOne({ id }); } - // async create(phone: string): Promise { - // const user = this.adminRepository.create({ phone }); - // await this.em.persistAndFlush(user); - // return user; - // } + async create(data: { phone: string; firstName?: string; lastName?: string; roleId: string; restId: string }) { + const { phone, firstName, lastName, roleId, restId } = data; + + // check existing + const existing = await this.adminRepository.findOne({ phone, restaurant: { id: restId } }); + if (existing) { + return existing; + } + + // load Role and Restaurant using the EntityManager + const role = await this.em.findOne(Role, { id: roleId }); + if (!role) throw new Error('Role not found'); + + const restaurant = await this.em.findOne(Restaurant, { id: restId }); + if (!restaurant) throw new Error('Restaurant not found'); + + const admin = this.adminRepository.create({ phone, firstName, lastName, role, restaurant }); + await this.em.persistAndFlush(admin); + return admin; + } } diff --git a/src/modules/admin/controllers/role.controller.ts b/src/modules/admin/controllers/role.controller.ts new file mode 100644 index 0000000..9313d73 --- /dev/null +++ b/src/modules/admin/controllers/role.controller.ts @@ -0,0 +1,52 @@ +import { Controller, Get, Post, Body, Param, Patch, Delete, Query } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiBody, ApiResponse } from '@nestjs/swagger'; +import { RoleService } from '../providers/role.service'; +import { CreateRoleDto } from '../dto/create-role.dto'; +import { UpdateRoleDto } from '../dto/update-role.dto'; +import { FindRolesDto } from '../dto/find-roles.dto'; + +@ApiTags('admin/roles') +@Controller('admin/roles') +export class RoleController { + constructor(private readonly roleService: RoleService) {} + + @Post() + @ApiOperation({ summary: 'Create a new role' }) + @ApiBody({ type: CreateRoleDto }) + @ApiResponse({ status: 201, description: 'Role created successfully' }) + async create(@Body() dto: CreateRoleDto) { + return this.roleService.create(dto); + } + + @Get() + @ApiOperation({ summary: 'Get all roles with pagination and filters' }) + @ApiResponse({ status: 200, description: 'Roles retrieved successfully' }) + async findAll(@Query() dto: FindRolesDto) { + return this.roleService.findAll(dto); + } + + @Get(':id') + @ApiOperation({ summary: 'Get a specific role by ID' }) + @ApiResponse({ status: 200, description: 'Role retrieved successfully' }) + @ApiResponse({ status: 404, description: 'Role not found' }) + async findOne(@Param('id') id: string) { + return this.roleService.findOne(id); + } + + @Patch(':id') + @ApiOperation({ summary: 'Update a role' }) + @ApiBody({ type: UpdateRoleDto }) + @ApiResponse({ status: 200, description: 'Role updated successfully' }) + @ApiResponse({ status: 404, description: 'Role not found' }) + async update(@Param('id') id: string, @Body() dto: UpdateRoleDto) { + return this.roleService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: 'Delete a role' }) + @ApiResponse({ status: 200, description: 'Role deleted successfully' }) + @ApiResponse({ status: 404, description: 'Role not found' }) + async remove(@Param('id') id: string) { + return this.roleService.remove(id); + } +} diff --git a/src/modules/admin/dto/create-admin.dto.ts b/src/modules/admin/dto/create-admin.dto.ts new file mode 100644 index 0000000..713b2f7 --- /dev/null +++ b/src/modules/admin/dto/create-admin.dto.ts @@ -0,0 +1,29 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class CreateAdminDto { + @ApiProperty({ description: 'Mobile phone number' }) + @IsNotEmpty() + @IsString() + phone!: string; + + @ApiProperty({ description: 'First name', required: false }) + @IsOptional() + @IsString() + firstName?: string; + + @ApiProperty({ description: 'Last name', required: false }) + @IsOptional() + @IsString() + lastName?: string; + + @ApiProperty({ description: 'Role id for the admin' }) + @IsNotEmpty() + @IsString() + roleId!: string; + + @ApiProperty({ description: 'Restaurant id' }) + @IsNotEmpty() + @IsString() + restId!: string; +} diff --git a/src/modules/admin/dto/create-role.dto.ts b/src/modules/admin/dto/create-role.dto.ts new file mode 100644 index 0000000..7eaca81 --- /dev/null +++ b/src/modules/admin/dto/create-role.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString, IsArray, IsOptional } from 'class-validator'; + +export class CreateRoleDto { + @ApiProperty({ description: 'Role name' }) + @IsNotEmpty() + @IsString() + name!: string; + + @ApiProperty({ description: 'Restaurant id (optional)', required: false }) + @IsOptional() + @IsString() + restId?: string; + + @ApiProperty({ description: 'List of permission IDs', isArray: true, required: false }) + @IsOptional() + @IsArray() + permissionIds?: string[]; +} diff --git a/src/modules/admin/dto/find-roles.dto.ts b/src/modules/admin/dto/find-roles.dto.ts new file mode 100644 index 0000000..19518cd --- /dev/null +++ b/src/modules/admin/dto/find-roles.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional, IsString, IsNumber } from 'class-validator'; + +export class FindRolesDto { + @ApiProperty({ description: 'Search by role name', required: false }) + @IsOptional() + @IsString() + name?: string; + + @ApiProperty({ description: 'Restaurant id filter', required: false }) + @IsOptional() + @IsString() + restId?: string; + + @ApiProperty({ description: 'Page number', required: false, default: 1 }) + @IsOptional() + @IsNumber() + page?: number; + + @ApiProperty({ description: 'Items per page', required: false, default: 20 }) + @IsOptional() + @IsNumber() + limit?: number; +} diff --git a/src/modules/admin/dto/update-role.dto.ts b/src/modules/admin/dto/update-role.dto.ts new file mode 100644 index 0000000..ccf9939 --- /dev/null +++ b/src/modules/admin/dto/update-role.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional, IsString, IsArray } from 'class-validator'; + +export class UpdateRoleDto { + @ApiProperty({ description: 'Role name', required: false }) + @IsOptional() + @IsString() + name?: string; + + @ApiProperty({ description: 'List of permission IDs', isArray: true, required: false }) + @IsOptional() + @IsArray() + permissionIds?: string[]; +} diff --git a/src/modules/admin/providers/role.service.ts b/src/modules/admin/providers/role.service.ts new file mode 100644 index 0000000..bddc2aa --- /dev/null +++ b/src/modules/admin/providers/role.service.ts @@ -0,0 +1,129 @@ +import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; +import { InjectRepository } from '@mikro-orm/nestjs'; +import { EntityRepository } from '@mikro-orm/core'; +import { Role } from '../entities/role.entity'; +import { Permission } from '../entities/permission.entity'; +import { Restaurant } from '../../restaurants/entities/restaurant.entity'; +import { EntityManager } from '@mikro-orm/postgresql'; +import { CreateRoleDto } from '../dto/create-role.dto'; +import { UpdateRoleDto } from '../dto/update-role.dto'; +import { FindRolesDto } from '../dto/find-roles.dto'; + +@Injectable() +export class RoleService { + constructor( + @InjectRepository(Role) + private readonly roleRepository: EntityRepository, + @InjectRepository(Permission) + private readonly permissionRepository: EntityRepository, + private readonly em: EntityManager, + ) {} + + async create(dto: CreateRoleDto) { + const { name, restId, permissionIds } = dto; + + // Check if role already exists + const existing = await this.roleRepository.findOne({ name, restaurant: restId ? { id: restId } : null }); + if (existing) { + throw new BadRequestException('Role with this name already exists for the restaurant'); + } + + let restaurant: Restaurant | null = null; + if (restId) { + restaurant = await this.em.findOne(Restaurant, { id: restId }); + if (!restaurant) { + throw new NotFoundException('Restaurant not found'); + } + } + + const role = this.roleRepository.create({ + name, + restaurant, + }); + + // Add permissions if provided + if (permissionIds && permissionIds.length > 0) { + const permissions = await this.permissionRepository.find({ id: { $in: permissionIds } }); + if (permissions.length !== permissionIds.length) { + throw new BadRequestException('One or more permissions not found'); + } + permissions.forEach(p => role.permissions.add(p)); + } + + await this.em.persistAndFlush(role); + return role; + } + + async findAll(dto: FindRolesDto) { + const { name, restId, page = 1, limit = 20 } = dto; + const offset = (page - 1) * limit; + + // Build query dynamically + const query: Record = restId ? { restaurant: { id: restId } } : {}; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const [roles, count] = await this.roleRepository.findAndCount(query as any, { + offset, + limit, + populate: ['permissions', 'restaurant'], + }); + + // Filter by name in memory if provided + let filtered = roles; + if (name) { + filtered = roles.filter(r => r.name.toLowerCase().includes(name.toLowerCase())); + } + + return { + data: filtered, + total: count, + page, + limit, + totalPages: Math.ceil(count / limit), + }; + } + + async findOne(id: string) { + const role = await this.roleRepository.findOne({ id }, { populate: ['permissions', 'restaurant'] }); + if (!role) { + throw new NotFoundException('Role not found'); + } + return role; + } + + async update(id: string, dto: UpdateRoleDto) { + const role = await this.roleRepository.findOne({ id }); + if (!role) { + throw new NotFoundException('Role not found'); + } + + if (dto.name) { + role.name = dto.name; + } + + if (dto.permissionIds && dto.permissionIds.length >= 0) { + // Clear existing permissions and add new ones + role.permissions.removeAll(); + const permissions = await this.permissionRepository.find({ id: { $in: dto.permissionIds } }); + if (permissions.length !== dto.permissionIds.length) { + throw new BadRequestException('One or more permissions not found'); + } + permissions.forEach(p => role.permissions.add(p)); + } + + await this.em.persistAndFlush(role); + return role; + } + + async remove(id: string) { + const role = await this.roleRepository.findOne({ id }); + if (!role) { + throw new NotFoundException('Role not found'); + } + + // Soft delete by removing from database or mark as deleted + // For now, we'll do a hard delete since Role entity doesn't have deletedAt + await this.em.removeAndFlush(role); + return { message: 'Role deleted successfully' }; + } +} diff --git a/src/modules/auth/controllers/admin-auth.controller.ts b/src/modules/auth/controllers/admin-auth.controller.ts index 7023e73..e43a537 100644 --- a/src/modules/auth/controllers/admin-auth.controller.ts +++ b/src/modules/auth/controllers/admin-auth.controller.ts @@ -19,7 +19,7 @@ export class AdminAuthController { @ApiResponse({ status: 201, description: 'OTP requested successfully' }) @ApiResponse({ status: 400, description: 'Invalid mobile number' }) otpRequest(@Body() dto: RequestOtpDto) { - return this.authService.requestOtp(dto); + return this.authService.requestOtpAdmin(dto); } @Post('otp/verify') @@ -28,7 +28,7 @@ export class AdminAuthController { @ApiResponse({ status: 200, description: 'OTP verified successfully' }) @ApiResponse({ status: 400, description: 'Invalid OTP or expired' }) otpVerify(@Body() dto: VerifyOtpDto) { - return this.authService.verifyOtp(dto.phone, dto.slug, dto.otp); // assuming dto has `otp` property + return this.authService.verifyOtpAdmin(dto.phone, dto.slug, dto.otp); // assuming dto has `otp` property } // @Post('admin/otp/request') diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index cf588be..b4ba414 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -71,7 +71,7 @@ export class AuthService { } async verifyOtpAdmin(phone: string, slug: string, code: string) { - const cachedCode = await this.cacheService.get(`otp:${slug}:${phone}`); + const cachedCode = await this.cacheService.get(`otp-admin:${slug}:${phone}`); if (!cachedCode) throw new BadRequestException('OTP expired or not found'); if (cachedCode !== code) throw new BadRequestException('Invalid OTP'); diff --git a/src/modules/auth/services/tokens.service.ts b/src/modules/auth/services/tokens.service.ts index e3fba93..6f7979d 100755 --- a/src/modules/auth/services/tokens.service.ts +++ b/src/modules/auth/services/tokens.service.ts @@ -10,10 +10,10 @@ import { AuthMessage, UserMessage } from '../../../common/enums/message.enum'; import { RefreshToken } from '../../users/entities/refresh-token.entity'; import { User } from '../../users/entities/user.entity'; import { IAdminTokenPayload, ITokenPayload } from '../interfaces/IToken-payload'; -import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity'; -import { RestRepository } from 'src/modules/restaurants/repositories/rest.repository'; -import { Admin } from 'src/modules/admin/entities/admin.entity'; -import { CacheService } from 'src/modules/utils/cache.service'; +import { Restaurant } from '../../restaurants/entities/restaurant.entity'; +import { RestRepository } from '../../restaurants/repositories/rest.repository'; +import { Admin } from '../../admin/entities/admin.entity'; +import { CacheService } from '../../utils/cache.service'; @Injectable() export class TokensService { diff --git a/src/modules/foods/entities/food.entity.ts b/src/modules/foods/entities/food.entity.ts index a44e0a5..f2dfa4b 100644 --- a/src/modules/foods/entities/food.entity.ts +++ b/src/modules/foods/entities/food.entity.ts @@ -1,7 +1,7 @@ import { Collection, Entity, ManyToMany, OneToOne, Property } from '@mikro-orm/core'; import { Category } from './category.entity'; import { BaseEntity } from '../../../common/entities/base.entity'; -import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity'; +import { Restaurant } from '../../../modules/restaurants/entities/restaurant.entity'; @Entity({ tableName: 'foods' }) export class Food extends BaseEntity { diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index 69fbb76..5256907 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -1,7 +1,7 @@ import { Entity, Property, OneToMany, Collection, OneToOne } from '@mikro-orm/core'; import { RefreshToken } from './refresh-token.entity'; import { BaseEntity } from '../../../common/entities/base.entity'; -import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity'; +import { Restaurant } from '../../restaurants/entities/restaurant.entity'; @Entity({ tableName: 'users' }) export class User extends BaseEntity {