diff --git a/src/common/decorators/index.ts b/src/common/decorators/index.ts new file mode 100644 index 0000000..6a12fec --- /dev/null +++ b/src/common/decorators/index.ts @@ -0,0 +1,3 @@ +export { UserId } from './user-id.decorator'; +export { RestId } from './rest-id.decorator'; +export { RateLimit } from './rate-limit.decorator'; diff --git a/src/common/decorators/rest-id.decorator.ts b/src/common/decorators/rest-id.decorator.ts new file mode 100644 index 0000000..afebf8c --- /dev/null +++ b/src/common/decorators/rest-id.decorator.ts @@ -0,0 +1,18 @@ +import { createParamDecorator, type ExecutionContext } from '@nestjs/common'; +import type { Request } from 'express'; + +/** + * Decorator to extract restId from the authenticated request. + * Must be used after AdminAuthGuard or AuthGuard that sets request.restId. + * + * @example + * @Get('/restaurants') + * @UseGuards(AdminAuthGuard) + * getRestaurants(@RestId() restId: string) { + * return this.restaurantService.findById(restId); + * } + */ +export const RestId = createParamDecorator((data: unknown, ctx: ExecutionContext): string => { + const request = ctx.switchToHttp().getRequest(); + return request.restId || ''; +}); diff --git a/src/common/decorators/user-id.decorator.ts b/src/common/decorators/user-id.decorator.ts new file mode 100644 index 0000000..eb0c551 --- /dev/null +++ b/src/common/decorators/user-id.decorator.ts @@ -0,0 +1,18 @@ +import { createParamDecorator, type ExecutionContext } from '@nestjs/common'; +import type { Request } from 'express'; + +/** + * Decorator to extract userId from the authenticated request. + * Must be used after AdminAuthGuard or AuthGuard that sets request.userId. + * + * @example + * @Get('/profile') + * @UseGuards(AdminAuthGuard) + * getProfile(@UserId() userId: string) { + * return this.userService.findById(userId); + * } + */ +export const UserId = createParamDecorator((data: unknown, ctx: ExecutionContext): string => { + const request = ctx.switchToHttp().getRequest(); + return request.userId || ''; +}); diff --git a/src/modules/auth/guards/auth.guard.ts b/src/modules/auth/guards/adminAuth.guard.ts similarity index 94% rename from src/modules/auth/guards/auth.guard.ts rename to src/modules/auth/guards/adminAuth.guard.ts index e4a56bb..23da293 100644 --- a/src/modules/auth/guards/auth.guard.ts +++ b/src/modules/auth/guards/adminAuth.guard.ts @@ -10,7 +10,7 @@ export interface AuthRequest extends Request { } @Injectable() -export class AuthGuard implements CanActivate { +export class AdminAuthGuard implements CanActivate { constructor( private readonly jwtService: JwtService, private readonly configService: ConfigService, @@ -30,7 +30,6 @@ export class AuthGuard implements CanActivate { const payload = await this.jwtService.verifyAsync(token, { secret, }); - console.log('payload', payload); request['userId'] = payload.userId; request['restId'] = payload.restId; } catch { diff --git a/src/modules/restaurants/dto/create-schedule.dto.ts b/src/modules/restaurants/dto/create-schedule.dto.ts index e7de94c..41357c3 100644 --- a/src/modules/restaurants/dto/create-schedule.dto.ts +++ b/src/modules/restaurants/dto/create-schedule.dto.ts @@ -1,4 +1,4 @@ -import { IsNumber, IsBoolean, IsOptional, IsString, Min, Max } from 'class-validator'; +import { IsNumber, IsBoolean, IsOptional, IsString, Min, Max, Matches } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; @@ -10,12 +10,18 @@ export class CreateScheduleDto { @Type(() => Number) weekDay!: number; - @ApiProperty({ example: '08:00', description: 'زمان باز شدن (دقیقه از نیمه شب)' }) + @ApiProperty({ example: '08:00', description: 'زمان باز شدن (HH:MM, از ۰۰:۰۰ تا ۲۳:۵۹)' }) @IsString() + @Matches(/^([01]\d|2[0-3]):([0-5]\d)$/, { + message: 'openTime باید در فرمت HH:MM باشد (00:00 تا 23:59)', + }) openTime!: string; - @ApiProperty({ example: '23:00', description: 'زمان بستن (دقیقه از نیمه شب)' }) + @ApiProperty({ example: '23:00', description: 'زمان بستن (HH:MM, از ۰۰:۰۰ تا ۲۳:۵۹)' }) @IsString() + @Matches(/^([01]\d|2[0-3]):([0-5]\d)$/, { + message: 'closeTime باید در فرمت HH:MM باشد (00:00 تا 23:59)', + }) closeTime!: string; @ApiPropertyOptional({ example: true, description: 'آیا این برنامه فعال است؟' }) @@ -23,4 +29,8 @@ export class CreateScheduleDto { @IsBoolean() @Type(() => Boolean) isActive?: boolean; + + @ApiProperty({ example: 'rest-ulid-123', description: 'شناسه رستوران' }) + @IsString() + restId!: string; } diff --git a/src/modules/users/adminUser.controller.ts b/src/modules/users/adminUser.controller.ts new file mode 100644 index 0000000..b022a31 --- /dev/null +++ b/src/modules/users/adminUser.controller.ts @@ -0,0 +1,39 @@ +import { Controller, Get, UseGuards, Query, ValidationPipe } from '@nestjs/common'; +import { ApiTags, ApiBearerAuth, ApiOperation, ApiOkResponse } from '@nestjs/swagger'; +import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard'; +import { UserService } from './user.service'; +import { FindUsersDto } from './dto/find-user.dto'; +import { UserId } from 'src/common/decorators/user-id.decorator'; +import { RestId } from 'src/common/decorators'; + +@ApiTags('Admin/User') +@Controller('admin/user') +export class AdminUserController { + constructor(private readonly userService: UserService) {} + + @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'Get the current authenticated user profile' }) + @Get('/me') + async getUser(@UserId() userId: string) { + const user = await this.userService.findById(userId); + return { + message: `GET request: Retrieving profile for authenticated user`, + user, + userId, + }; + } + + @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'Get paginated list of users with filters' }) + @ApiOkResponse({ description: 'Paginated list of users' }) + @Get('/') + async findAll( + @Query(new ValidationPipe({ transform: true, whitelist: true })) + query: FindUsersDto, + @RestId() restId: string, + ) { + return this.userService.findAll(restId, query); + } +} diff --git a/src/modules/users/dto/find-user.dto.ts b/src/modules/users/dto/find-user.dto.ts index 8e60665..ba3852b 100644 --- a/src/modules/users/dto/find-user.dto.ts +++ b/src/modules/users/dto/find-user.dto.ts @@ -13,7 +13,7 @@ export class FindUsersDto { * @default 1 */ @ApiPropertyOptional({ - description: 'The page number to retrieve.', + description: 'شماره صفحه', type: Number, default: 1, minimum: 1, @@ -29,7 +29,7 @@ export class FindUsersDto { * @default 10 */ @ApiPropertyOptional({ - description: 'The number of items per page.', + description: 'تعداد مورد در هر صفحه', type: Number, default: 10, minimum: 1, @@ -45,9 +45,8 @@ export class FindUsersDto { * Searches firstName, lastName, phone, and nationalCode. */ @ApiPropertyOptional({ - description: 'A general search term (firstName, lastName, phone, nationalCode).', + description: 'جستجو بر اساس نام، نام‌خانوادگی یا شماره تماس', type: String, - example: 'John Doe', }) @IsOptional() @IsString() @@ -58,12 +57,9 @@ export class FindUsersDto { * @default "createdAt" */ @ApiPropertyOptional({ - description: 'The field to sort the results by.', + description: 'فیلد مرتب‌سازی', type: String, default: 'createdAt', - // You can provide a list of valid keys for better documentation - // You must ensure 'keyof User' is correctly exported/available here - example: 'lastName', }) @IsOptional() @IsString() @@ -74,8 +70,8 @@ export class FindUsersDto { * @default "desc" */ @ApiPropertyOptional({ - description: 'The direction to sort the results.', - enum: sortOrderOptions, // Use the constant array + description: 'جهت مرتب‌سازی (asc یا desc)', + enum: sortOrderOptions, default: 'desc', }) @IsOptional() diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index 868c7a1..69fbb76 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -1,9 +1,16 @@ -import { Entity, Property, OneToMany, Collection } from '@mikro-orm/core'; +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'; @Entity({ tableName: 'users' }) export class User extends BaseEntity { + @OneToOne(() => Restaurant, { owner: true }) + restaurant!: Restaurant; + + @OneToMany(() => RefreshToken, token => token.user) + refreshTokens = new Collection(this); + @Property() firstName!: string; @@ -33,7 +40,4 @@ export class User extends BaseEntity { @Property({ default: 0, type: 'int' }) points: number = 0; - - @OneToMany(() => RefreshToken, token => token.user) - refreshTokens = new Collection(this); } diff --git a/src/modules/users/user.controller.ts b/src/modules/users/user.controller.ts index 0ff54b4..9d488c3 100644 --- a/src/modules/users/user.controller.ts +++ b/src/modules/users/user.controller.ts @@ -1,39 +1,36 @@ -import { Controller, Get, Req, UseGuards, Patch, Body, ValidationPipe } from '@nestjs/common'; +import { Controller, Get, UseGuards, Patch, Body, ValidationPipe } from '@nestjs/common'; import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; -import { AuthGuard, type AuthRequest } from 'src/modules/auth/guards/auth.guard'; +import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard'; import { UserService } from './user.service'; import { UpdateUserDto } from './dto/update-user.dto'; +import { UserId } from 'src/common/decorators/user-id.decorator'; @ApiTags('User') @Controller('user') export class UserController { constructor(private readonly userService: UserService) {} - @UseGuards(AuthGuard) + @UseGuards(AdminAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Get the current authenticated user profile' }) @Get('/me') - async getUser(@Req() req: AuthRequest) { - const userId = req.userId; - const restId = req.restId; + async getUser(@UserId() userId: string) { const user = await this.userService.findById(userId); return { - message: `GET request: Retrieving profile for authenticated user `, + message: `GET request: Retrieving profile for authenticated user`, user, userId, - restId, }; } - // 2. Update User Specification (PATCH /user) - @UseGuards(AuthGuard) + + @UseGuards(AdminAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Update the authenticated user profile' }) @Patch('/') async updateUser( - @Req() req: AuthRequest, + @UserId() userId: string, @Body(new ValidationPipe({ transform: true, whitelist: true })) dto: UpdateUserDto, ) { - const userId = req.userId; const user = await this.userService.updateUser(userId, dto); return { message: `PATCH request: Updating profile for user ${userId}`, @@ -41,16 +38,4 @@ export class UserController { user, }; } - - // @UseGuards(AdminAuthGuard) - // @ApiBearerAuth() - // @ApiOperation({ summary: 'Users : Admin Route' }) - // @Get('/admin/users') - // async findAll( - // // Use ValidationPipe to validate and transform the DTO - // @Query(new ValidationPipe({ transform: true, whitelist: true })) - // query: FindUsersDto, - // ) { - // return this.userService.findAll(query); - // } } diff --git a/src/modules/users/user.module.ts b/src/modules/users/user.module.ts index f5f8cd9..6831bf0 100644 --- a/src/modules/users/user.module.ts +++ b/src/modules/users/user.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; import { UserService } from './user.service'; -import { UserController } from './user.controller'; +import { AdminUserController } from './adminUser.controller'; import { MikroOrmModule } from '@mikro-orm/nestjs'; import { User } from './entities/user.entity'; import { JwtModule } from '@nestjs/jwt'; @@ -8,7 +8,7 @@ import { UserRepository } from './repositories/user.repository'; @Module({ providers: [UserService, UserRepository], - controllers: [UserController], + controllers: [AdminUserController], imports: [MikroOrmModule.forFeature([User]), JwtModule], exports: [UserService, UserRepository], }) diff --git a/src/modules/users/user.service.ts b/src/modules/users/user.service.ts index dff64fd..661d90a 100644 --- a/src/modules/users/user.service.ts +++ b/src/modules/users/user.service.ts @@ -96,14 +96,14 @@ export class UserService { return user; } - async findAll(dto: FindUsersDto): Promise> { + async findAll(restId: string, dto: FindUsersDto): Promise> { const { page = 1, limit = 10, search, orderBy = 'createdAt', order = 'desc' } = dto; // 1. Calculate pagination const offset = (page - 1) * limit; // 2. Build the 'where' filter query - const where: FilterQuery = {}; + const where: FilterQuery = { restaurant: { id: restId } }; // 4. Add 'search' logic (case-insensitive) if (search) {