From b2d81446b748a65271177c5330753ba38d115f11 Mon Sep 17 00:00:00 2001 From: DrMesta103 Date: Sun, 29 Mar 2026 20:28:43 +0300 Subject: [PATCH] complet auth api --- src/modules/auth/auth.controller.ts | 12 +++- src/modules/auth/dto/login-password.dto.ts | 3 + src/modules/auth/dto/register-password.dto.ts | 5 ++ src/modules/auth/dto/request-otp.dto.ts | 3 + src/modules/auth/dto/verify-otp.dto.ts | 3 + .../catalog/dto/public-filter-products.dto.ts | 72 +++++++++++++++++++ .../catalog/product-details.controller.ts | 12 ++-- src/modules/catalog/products.controller.ts | 63 +++++++++++----- 8 files changed, 150 insertions(+), 23 deletions(-) create mode 100644 src/modules/catalog/dto/public-filter-products.dto.ts diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index e1f49128..26b51ede 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -7,7 +7,7 @@ import { UseGuards, } from '@nestjs/common'; import { Request } from 'express'; -import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiBody, ApiOperation, ApiTags } from '@nestjs/swagger'; import { Permissions } from '../../common/decorators/permissions.decorator'; import { Roles } from '../../common/decorators/roles.decorator'; import { PermissionsGuard } from '../../common/guards/permissions.guard'; @@ -28,26 +28,36 @@ export class AuthController { constructor(private readonly authService: AuthService) {} @Post('otp/request') + @ApiOperation({ summary: 'Request OTP for mobile auth flow' }) + @ApiBody({ type: RequestOtpDto }) requestOtp(@Body() dto: RequestOtpDto) { return this.authService.requestOtp(dto.phone, dto.fullName); } @Post('register/password') + @ApiOperation({ summary: 'Register with mobile, username, and password' }) + @ApiBody({ type: RegisterPasswordDto }) registerWithPassword(@Body() dto: RegisterPasswordDto) { return this.authService.registerWithPassword(dto); } @Post('login/password') + @ApiOperation({ summary: 'Login with username and password' }) + @ApiBody({ type: LoginPasswordDto }) loginWithPassword(@Body() dto: LoginPasswordDto) { return this.authService.loginWithPassword(dto); } @Post('otp/verify') + @ApiOperation({ summary: 'Verify OTP code for mobile auth flow' }) + @ApiBody({ type: VerifyOtpDto }) verifyOtp(@Body() dto: VerifyOtpDto) { return this.authService.verifyOtp(dto.phone, dto.otp); } @Post('refresh') + @ApiOperation({ summary: 'Refresh JWT access token' }) + @ApiBody({ type: RefreshTokenDto }) refresh(@Body() dto: RefreshTokenDto) { return this.authService.refreshToken(dto.refreshToken); } diff --git a/src/modules/auth/dto/login-password.dto.ts b/src/modules/auth/dto/login-password.dto.ts index b64205b9..44b6f09d 100644 --- a/src/modules/auth/dto/login-password.dto.ts +++ b/src/modules/auth/dto/login-password.dto.ts @@ -1,11 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; import { IsString, MaxLength, MinLength } from 'class-validator'; export class LoginPasswordDto { + @ApiProperty({ example: 'alireza' }) @IsString() @MinLength(3) @MaxLength(50) username: string; + @ApiProperty({ example: 'StrongPass123' }) @IsString() @MinLength(6) @MaxLength(100) diff --git a/src/modules/auth/dto/register-password.dto.ts b/src/modules/auth/dto/register-password.dto.ts index 2475ed45..906a27ec 100644 --- a/src/modules/auth/dto/register-password.dto.ts +++ b/src/modules/auth/dto/register-password.dto.ts @@ -1,19 +1,24 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsOptional, IsString, Matches, MaxLength, MinLength } from 'class-validator'; export class RegisterPasswordDto { + @ApiProperty({ example: '989121234567' }) @Matches(/^\+?[1-9]\d{7,14}$/) phone: string; + @ApiProperty({ example: 'alireza' }) @IsString() @MinLength(3) @MaxLength(50) username: string; + @ApiProperty({ example: 'StrongPass123' }) @IsString() @MinLength(6) @MaxLength(100) password: string; + @ApiPropertyOptional({ example: 'علی رضایی' }) @IsOptional() @IsString() @MinLength(2) diff --git a/src/modules/auth/dto/request-otp.dto.ts b/src/modules/auth/dto/request-otp.dto.ts index 9c0b1dd0..6fd9f211 100644 --- a/src/modules/auth/dto/request-otp.dto.ts +++ b/src/modules/auth/dto/request-otp.dto.ts @@ -1,9 +1,12 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsOptional, IsString, Matches, MaxLength, MinLength } from 'class-validator'; export class RequestOtpDto { + @ApiProperty({ example: '989121234567' }) @Matches(/^\+?[1-9]\d{7,14}$/) phone: string; + @ApiPropertyOptional({ example: 'علی رضایی' }) @IsOptional() @IsString() @MinLength(2) diff --git a/src/modules/auth/dto/verify-otp.dto.ts b/src/modules/auth/dto/verify-otp.dto.ts index 91d3d4f9..d19dcb23 100644 --- a/src/modules/auth/dto/verify-otp.dto.ts +++ b/src/modules/auth/dto/verify-otp.dto.ts @@ -1,9 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; import { IsString, Length, Matches } from 'class-validator'; export class VerifyOtpDto { + @ApiProperty({ example: '989121234567' }) @Matches(/^\+?[1-9]\d{7,14}$/) phone: string; + @ApiProperty({ example: '12345' }) @IsString() @Length(4, 6) otp: string; diff --git a/src/modules/catalog/dto/public-filter-products.dto.ts b/src/modules/catalog/dto/public-filter-products.dto.ts new file mode 100644 index 00000000..e95d7a56 --- /dev/null +++ b/src/modules/catalog/dto/public-filter-products.dto.ts @@ -0,0 +1,72 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { + IsArray, + IsBoolean, + IsEnum, + IsInt, + IsObject, + IsOptional, + IsString, + IsUUID, + Min, +} from 'class-validator'; +import { parseJsonValue } from '../../../common/utils/json-transform.util'; +import { ProductListSort } from '../enums/product-list-sort.enum'; + +export class PublicFilterProductsDto { + @ApiPropertyOptional() + @IsOptional() + @IsString() + search?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsUUID() + categoryId?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsUUID() + brandId?: string; + + @ApiPropertyOptional({ type: Object }) + @IsOptional() + @Transform(parseJsonValue) + @IsObject() + attributes?: Record; + + @ApiPropertyOptional({ type: [String] }) + @IsOptional() + @Transform(parseJsonValue) + @IsArray() + @IsString({ each: true }) + tags?: string[]; + + @ApiPropertyOptional() + @IsOptional() + @Transform(({ value }) => + typeof value === 'boolean' ? value : String(value).toLowerCase() === 'true', + ) + @IsBoolean() + featured?: boolean; + + @ApiPropertyOptional({ enum: ProductListSort }) + @IsOptional() + @IsEnum(ProductListSort) + sort?: ProductListSort; + + @ApiPropertyOptional({ default: 1 }) + @IsOptional() + @Transform(({ value }) => Number(value)) + @IsInt() + @Min(1) + page?: number = 1; + + @ApiPropertyOptional({ default: 20 }) + @IsOptional() + @Transform(({ value }) => Number(value)) + @IsInt() + @Min(1) + limit?: number = 20; +} diff --git a/src/modules/catalog/product-details.controller.ts b/src/modules/catalog/product-details.controller.ts index e861e958..c3648b11 100644 --- a/src/modules/catalog/product-details.controller.ts +++ b/src/modules/catalog/product-details.controller.ts @@ -7,12 +7,6 @@ import { ProductsService } from './products.service'; @ApiTags('Products') @ApiBearerAuth() -@ApiHeader({ - name: 'x-product-type', - required: false, - description: 'Optional product type filter header. Falls back to query param `type` if omitted.', - enum: ['Industrial', 'Automotive'], -}) @UseGuards(OptionalJwtAuthGuard) @Controller('product') export class ProductDetailsController { @@ -20,6 +14,12 @@ export class ProductDetailsController { @Get(':slug') @ApiOperation({ summary: 'Get one published product by slug' }) + @ApiHeader({ + name: 'x-product-type', + required: false, + description: 'Product type header for public product APIs.', + enum: ['Industrial', 'Automotive'], + }) findBySlug( @Param('slug') slug: string, @Headers('x-product-type') productType?: string, diff --git a/src/modules/catalog/products.controller.ts b/src/modules/catalog/products.controller.ts index 028c69d0..c695fdb2 100644 --- a/src/modules/catalog/products.controller.ts +++ b/src/modules/catalog/products.controller.ts @@ -21,16 +21,11 @@ import { JwtPayload } from '../auth/interfaces/jwt-payload.interface'; import { OptionalJwtAuthGuard } from '../auth/guards/optional-jwt-auth.guard'; import { CreateProductReviewDto } from './dto/create-product-review.dto'; import { FilterProductsDto } from './dto/filter-products.dto'; +import { PublicFilterProductsDto } from './dto/public-filter-products.dto'; import { ProductsService } from './products.service'; @ApiTags('Products') @ApiBearerAuth() -@ApiHeader({ - name: 'x-product-type', - required: false, - description: 'Optional product type filter header. Falls back to query param `type` if omitted.', - enum: ['Industrial', 'Automotive'], -}) @UseGuards(OptionalJwtAuthGuard) @Controller('products') export class ProductsController { @@ -38,25 +33,37 @@ export class ProductsController { @Get() @ApiOperation({ summary: 'List published products for storefront' }) + @ApiHeader({ + name: 'x-product-type', + required: false, + description: 'Product type header for public product APIs.', + enum: ['Industrial', 'Automotive'], + }) findAll( - @Query() filters: FilterProductsDto, + @Query() filters: PublicFilterProductsDto, @Headers('x-product-type') productType?: string, @Req() request?: Request & { user?: JwtPayload | null }, ) { - return this.productsService.findPublic(filters, productType, request?.user); + return this.productsService.findPublic(filters as FilterProductsDto, productType, request?.user); } @Get('brands/:brandSlug') @ApiOperation({ summary: 'List published products by brand slug' }) + @ApiHeader({ + name: 'x-product-type', + required: false, + description: 'Product type header for public product APIs.', + enum: ['Industrial', 'Automotive'], + }) findByBrandSlug( @Param('brandSlug') brandSlug: string, - @Query() filters: FilterProductsDto, + @Query() filters: PublicFilterProductsDto, @Headers('x-product-type') productType?: string, @Req() request?: Request & { user?: JwtPayload | null }, ) { return this.productsService.findPublicByBrandSlug( brandSlug, - filters, + filters as FilterProductsDto, productType, request?.user, ); @@ -64,29 +71,41 @@ export class ProductsController { @Get('brands/:brandSlug/filters') @ApiOperation({ summary: 'Get available brand page filters by brand slug' }) + @ApiHeader({ + name: 'x-product-type', + required: false, + description: 'Product type header for public product APIs.', + enum: ['Industrial', 'Automotive'], + }) findBrandFilters( @Param('brandSlug') brandSlug: string, - @Query() filters: FilterProductsDto, + @Query() filters: PublicFilterProductsDto, @Headers('x-product-type') productType?: string, ) { return this.productsService.findPublicBrandFilters( brandSlug, - filters, + filters as FilterProductsDto, productType, ); } @Get('categories/:categorySlug') @ApiOperation({ summary: 'List published products by category slug' }) + @ApiHeader({ + name: 'x-product-type', + required: false, + description: 'Product type header for public product APIs.', + enum: ['Industrial', 'Automotive'], + }) findByCategorySlug( @Param('categorySlug') categorySlug: string, - @Query() filters: FilterProductsDto, + @Query() filters: PublicFilterProductsDto, @Headers('x-product-type') productType?: string, @Req() request?: Request & { user?: JwtPayload | null }, ) { return this.productsService.findPublicByCategorySlug( categorySlug, - filters, + filters as FilterProductsDto, productType, request?.user, ); @@ -94,14 +113,20 @@ export class ProductsController { @Get('categories/:categorySlug/filters') @ApiOperation({ summary: 'Get available category page filters by category slug' }) + @ApiHeader({ + name: 'x-product-type', + required: false, + description: 'Product type header for public product APIs.', + enum: ['Industrial', 'Automotive'], + }) findCategoryFilters( @Param('categorySlug') categorySlug: string, - @Query() filters: FilterProductsDto, + @Query() filters: PublicFilterProductsDto, @Headers('x-product-type') productType?: string, ) { return this.productsService.findPublicCategoryFilters( categorySlug, - filters, + filters as FilterProductsDto, productType, ); } @@ -121,6 +146,12 @@ export class ProductsController { @Get(':id') @ApiOperation({ summary: 'Get one published product with approved reviews summary' }) + @ApiHeader({ + name: 'x-product-type', + required: false, + description: 'Product type header for public product APIs.', + enum: ['Industrial', 'Automotive'], + }) findOne( @Param('id') id: string, @Headers('x-product-type') productType?: string,