complet auth api
This commit is contained in:
@@ -7,7 +7,7 @@ import {
|
|||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Request } from 'express';
|
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 { Permissions } from '../../common/decorators/permissions.decorator';
|
||||||
import { Roles } from '../../common/decorators/roles.decorator';
|
import { Roles } from '../../common/decorators/roles.decorator';
|
||||||
import { PermissionsGuard } from '../../common/guards/permissions.guard';
|
import { PermissionsGuard } from '../../common/guards/permissions.guard';
|
||||||
@@ -28,26 +28,36 @@ export class AuthController {
|
|||||||
constructor(private readonly authService: AuthService) {}
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
|
||||||
@Post('otp/request')
|
@Post('otp/request')
|
||||||
|
@ApiOperation({ summary: 'Request OTP for mobile auth flow' })
|
||||||
|
@ApiBody({ type: RequestOtpDto })
|
||||||
requestOtp(@Body() dto: RequestOtpDto) {
|
requestOtp(@Body() dto: RequestOtpDto) {
|
||||||
return this.authService.requestOtp(dto.phone, dto.fullName);
|
return this.authService.requestOtp(dto.phone, dto.fullName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('register/password')
|
@Post('register/password')
|
||||||
|
@ApiOperation({ summary: 'Register with mobile, username, and password' })
|
||||||
|
@ApiBody({ type: RegisterPasswordDto })
|
||||||
registerWithPassword(@Body() dto: RegisterPasswordDto) {
|
registerWithPassword(@Body() dto: RegisterPasswordDto) {
|
||||||
return this.authService.registerWithPassword(dto);
|
return this.authService.registerWithPassword(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('login/password')
|
@Post('login/password')
|
||||||
|
@ApiOperation({ summary: 'Login with username and password' })
|
||||||
|
@ApiBody({ type: LoginPasswordDto })
|
||||||
loginWithPassword(@Body() dto: LoginPasswordDto) {
|
loginWithPassword(@Body() dto: LoginPasswordDto) {
|
||||||
return this.authService.loginWithPassword(dto);
|
return this.authService.loginWithPassword(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('otp/verify')
|
@Post('otp/verify')
|
||||||
|
@ApiOperation({ summary: 'Verify OTP code for mobile auth flow' })
|
||||||
|
@ApiBody({ type: VerifyOtpDto })
|
||||||
verifyOtp(@Body() dto: VerifyOtpDto) {
|
verifyOtp(@Body() dto: VerifyOtpDto) {
|
||||||
return this.authService.verifyOtp(dto.phone, dto.otp);
|
return this.authService.verifyOtp(dto.phone, dto.otp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('refresh')
|
@Post('refresh')
|
||||||
|
@ApiOperation({ summary: 'Refresh JWT access token' })
|
||||||
|
@ApiBody({ type: RefreshTokenDto })
|
||||||
refresh(@Body() dto: RefreshTokenDto) {
|
refresh(@Body() dto: RefreshTokenDto) {
|
||||||
return this.authService.refreshToken(dto.refreshToken);
|
return this.authService.refreshToken(dto.refreshToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsString, MaxLength, MinLength } from 'class-validator';
|
import { IsString, MaxLength, MinLength } from 'class-validator';
|
||||||
|
|
||||||
export class LoginPasswordDto {
|
export class LoginPasswordDto {
|
||||||
|
@ApiProperty({ example: 'alireza' })
|
||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(3)
|
@MinLength(3)
|
||||||
@MaxLength(50)
|
@MaxLength(50)
|
||||||
username: string;
|
username: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'StrongPass123' })
|
||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(6)
|
@MinLength(6)
|
||||||
@MaxLength(100)
|
@MaxLength(100)
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { IsOptional, IsString, Matches, MaxLength, MinLength } from 'class-validator';
|
import { IsOptional, IsString, Matches, MaxLength, MinLength } from 'class-validator';
|
||||||
|
|
||||||
export class RegisterPasswordDto {
|
export class RegisterPasswordDto {
|
||||||
|
@ApiProperty({ example: '989121234567' })
|
||||||
@Matches(/^\+?[1-9]\d{7,14}$/)
|
@Matches(/^\+?[1-9]\d{7,14}$/)
|
||||||
phone: string;
|
phone: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'alireza' })
|
||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(3)
|
@MinLength(3)
|
||||||
@MaxLength(50)
|
@MaxLength(50)
|
||||||
username: string;
|
username: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'StrongPass123' })
|
||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(6)
|
@MinLength(6)
|
||||||
@MaxLength(100)
|
@MaxLength(100)
|
||||||
password: string;
|
password: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 'علی رضایی' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(2)
|
@MinLength(2)
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { IsOptional, IsString, Matches, MaxLength, MinLength } from 'class-validator';
|
import { IsOptional, IsString, Matches, MaxLength, MinLength } from 'class-validator';
|
||||||
|
|
||||||
export class RequestOtpDto {
|
export class RequestOtpDto {
|
||||||
|
@ApiProperty({ example: '989121234567' })
|
||||||
@Matches(/^\+?[1-9]\d{7,14}$/)
|
@Matches(/^\+?[1-9]\d{7,14}$/)
|
||||||
phone: string;
|
phone: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 'علی رضایی' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(2)
|
@MinLength(2)
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsString, Length, Matches } from 'class-validator';
|
import { IsString, Length, Matches } from 'class-validator';
|
||||||
|
|
||||||
export class VerifyOtpDto {
|
export class VerifyOtpDto {
|
||||||
|
@ApiProperty({ example: '989121234567' })
|
||||||
@Matches(/^\+?[1-9]\d{7,14}$/)
|
@Matches(/^\+?[1-9]\d{7,14}$/)
|
||||||
phone: string;
|
phone: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '12345' })
|
||||||
@IsString()
|
@IsString()
|
||||||
@Length(4, 6)
|
@Length(4, 6)
|
||||||
otp: string;
|
otp: string;
|
||||||
|
|||||||
72
src/modules/catalog/dto/public-filter-products.dto.ts
Normal file
72
src/modules/catalog/dto/public-filter-products.dto.ts
Normal file
@@ -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<string, unknown>;
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
@@ -7,12 +7,6 @@ import { ProductsService } from './products.service';
|
|||||||
|
|
||||||
@ApiTags('Products')
|
@ApiTags('Products')
|
||||||
@ApiBearerAuth()
|
@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)
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
@Controller('product')
|
@Controller('product')
|
||||||
export class ProductDetailsController {
|
export class ProductDetailsController {
|
||||||
@@ -20,6 +14,12 @@ export class ProductDetailsController {
|
|||||||
|
|
||||||
@Get(':slug')
|
@Get(':slug')
|
||||||
@ApiOperation({ summary: 'Get one published product by 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(
|
findBySlug(
|
||||||
@Param('slug') slug: string,
|
@Param('slug') slug: string,
|
||||||
@Headers('x-product-type') productType?: string,
|
@Headers('x-product-type') productType?: string,
|
||||||
|
|||||||
@@ -21,16 +21,11 @@ import { JwtPayload } from '../auth/interfaces/jwt-payload.interface';
|
|||||||
import { OptionalJwtAuthGuard } from '../auth/guards/optional-jwt-auth.guard';
|
import { OptionalJwtAuthGuard } from '../auth/guards/optional-jwt-auth.guard';
|
||||||
import { CreateProductReviewDto } from './dto/create-product-review.dto';
|
import { CreateProductReviewDto } from './dto/create-product-review.dto';
|
||||||
import { FilterProductsDto } from './dto/filter-products.dto';
|
import { FilterProductsDto } from './dto/filter-products.dto';
|
||||||
|
import { PublicFilterProductsDto } from './dto/public-filter-products.dto';
|
||||||
import { ProductsService } from './products.service';
|
import { ProductsService } from './products.service';
|
||||||
|
|
||||||
@ApiTags('Products')
|
@ApiTags('Products')
|
||||||
@ApiBearerAuth()
|
@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)
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
@Controller('products')
|
@Controller('products')
|
||||||
export class ProductsController {
|
export class ProductsController {
|
||||||
@@ -38,25 +33,37 @@ export class ProductsController {
|
|||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOperation({ summary: 'List published products for storefront' })
|
@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(
|
findAll(
|
||||||
@Query() filters: FilterProductsDto,
|
@Query() filters: PublicFilterProductsDto,
|
||||||
@Headers('x-product-type') productType?: string,
|
@Headers('x-product-type') productType?: string,
|
||||||
@Req() request?: Request & { user?: JwtPayload | null },
|
@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')
|
@Get('brands/:brandSlug')
|
||||||
@ApiOperation({ summary: 'List published products by brand slug' })
|
@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(
|
findByBrandSlug(
|
||||||
@Param('brandSlug') brandSlug: string,
|
@Param('brandSlug') brandSlug: string,
|
||||||
@Query() filters: FilterProductsDto,
|
@Query() filters: PublicFilterProductsDto,
|
||||||
@Headers('x-product-type') productType?: string,
|
@Headers('x-product-type') productType?: string,
|
||||||
@Req() request?: Request & { user?: JwtPayload | null },
|
@Req() request?: Request & { user?: JwtPayload | null },
|
||||||
) {
|
) {
|
||||||
return this.productsService.findPublicByBrandSlug(
|
return this.productsService.findPublicByBrandSlug(
|
||||||
brandSlug,
|
brandSlug,
|
||||||
filters,
|
filters as FilterProductsDto,
|
||||||
productType,
|
productType,
|
||||||
request?.user,
|
request?.user,
|
||||||
);
|
);
|
||||||
@@ -64,29 +71,41 @@ export class ProductsController {
|
|||||||
|
|
||||||
@Get('brands/:brandSlug/filters')
|
@Get('brands/:brandSlug/filters')
|
||||||
@ApiOperation({ summary: 'Get available brand page filters by brand slug' })
|
@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(
|
findBrandFilters(
|
||||||
@Param('brandSlug') brandSlug: string,
|
@Param('brandSlug') brandSlug: string,
|
||||||
@Query() filters: FilterProductsDto,
|
@Query() filters: PublicFilterProductsDto,
|
||||||
@Headers('x-product-type') productType?: string,
|
@Headers('x-product-type') productType?: string,
|
||||||
) {
|
) {
|
||||||
return this.productsService.findPublicBrandFilters(
|
return this.productsService.findPublicBrandFilters(
|
||||||
brandSlug,
|
brandSlug,
|
||||||
filters,
|
filters as FilterProductsDto,
|
||||||
productType,
|
productType,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('categories/:categorySlug')
|
@Get('categories/:categorySlug')
|
||||||
@ApiOperation({ summary: 'List published products by category slug' })
|
@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(
|
findByCategorySlug(
|
||||||
@Param('categorySlug') categorySlug: string,
|
@Param('categorySlug') categorySlug: string,
|
||||||
@Query() filters: FilterProductsDto,
|
@Query() filters: PublicFilterProductsDto,
|
||||||
@Headers('x-product-type') productType?: string,
|
@Headers('x-product-type') productType?: string,
|
||||||
@Req() request?: Request & { user?: JwtPayload | null },
|
@Req() request?: Request & { user?: JwtPayload | null },
|
||||||
) {
|
) {
|
||||||
return this.productsService.findPublicByCategorySlug(
|
return this.productsService.findPublicByCategorySlug(
|
||||||
categorySlug,
|
categorySlug,
|
||||||
filters,
|
filters as FilterProductsDto,
|
||||||
productType,
|
productType,
|
||||||
request?.user,
|
request?.user,
|
||||||
);
|
);
|
||||||
@@ -94,14 +113,20 @@ export class ProductsController {
|
|||||||
|
|
||||||
@Get('categories/:categorySlug/filters')
|
@Get('categories/:categorySlug/filters')
|
||||||
@ApiOperation({ summary: 'Get available category page filters by category slug' })
|
@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(
|
findCategoryFilters(
|
||||||
@Param('categorySlug') categorySlug: string,
|
@Param('categorySlug') categorySlug: string,
|
||||||
@Query() filters: FilterProductsDto,
|
@Query() filters: PublicFilterProductsDto,
|
||||||
@Headers('x-product-type') productType?: string,
|
@Headers('x-product-type') productType?: string,
|
||||||
) {
|
) {
|
||||||
return this.productsService.findPublicCategoryFilters(
|
return this.productsService.findPublicCategoryFilters(
|
||||||
categorySlug,
|
categorySlug,
|
||||||
filters,
|
filters as FilterProductsDto,
|
||||||
productType,
|
productType,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -121,6 +146,12 @@ export class ProductsController {
|
|||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@ApiOperation({ summary: 'Get one published product with approved reviews summary' })
|
@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(
|
findOne(
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@Headers('x-product-type') productType?: string,
|
@Headers('x-product-type') productType?: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user