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

View File

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

View File

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

View File

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

View File

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

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

View File

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