complet auth api

This commit is contained in:
2026-03-29 20:28:43 +03:00
parent 0c265ed0fc
commit b2d81446b7
8 changed files with 150 additions and 23 deletions

View File

@@ -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);
} }

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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;

View 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;
}

View File

@@ -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,

View File

@@ -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,