diff --git a/.env.example b/.env.example index 96a0d37..a353fc5 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,7 @@ DATABASE_URL=postgresql://mana:mana_secret@localhost:5436/mana_collection # API PORT=3002 PUBLIC_SITE_ORIGIN=http://mana-collection.com:3000 -CORS_ORIGIN=http://localhost:3000,http://127.0.0.1:3000,http://mana-collection.com:3000,http://www.mana-collection.com:3000,http://store.mana-collection.com:3000,http://customer.mana-collection.com:3000,http://admin.mana-collection.com:3000,https://manacollections.ir,https://store.manacollections.ir,https://customer.manacollections.ir,https://admin.manacollections.ir +CORS_ORIGIN=http://localhost:3000,http://127.0.0.1:3000,http://localhost:3001,http://127.0.0.1:3001,http://mana-collection.com:3000,http://www.mana-collection.com:3000,http://store.mana-collection.com:3001,http://customer.mana-collection.com:3001,http://admin.mana-collection.com:3001,https://manacollections.ir,https://store.manacollections.ir,https://customer.manacollections.ir,https://admin.manacollections.ir # JWT JWT_ACCESS_SECRET=change-me-mana-access-secret-min-32-chars diff --git a/prisma/migrations/20260907210000_product_vip_only/migration.sql b/prisma/migrations/20260907210000_product_vip_only/migration.sql new file mode 100644 index 0000000..e0a50cb --- /dev/null +++ b/prisma/migrations/20260907210000_product_vip_only/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "Product" ADD COLUMN "vipOnly" BOOLEAN NOT NULL DEFAULT false; + +-- CreateIndex +CREATE INDEX "Product_vipOnly_idx" ON "Product"("vipOnly"); diff --git a/prisma/migrations/20260907211000_product_vip_pricing/migration.sql b/prisma/migrations/20260907211000_product_vip_pricing/migration.sql new file mode 100644 index 0000000..76afcab --- /dev/null +++ b/prisma/migrations/20260907211000_product_vip_pricing/migration.sql @@ -0,0 +1,10 @@ +-- AlterTable +ALTER TABLE "Product" ADD COLUMN "vipEnabled" BOOLEAN NOT NULL DEFAULT false; +ALTER TABLE "Product" ADD COLUMN "vipSpecialPrice" BOOLEAN NOT NULL DEFAULT false; +ALTER TABLE "Product" ADD COLUMN "vipPrice" INTEGER; + +-- CreateIndex +CREATE INDEX "Product_vipEnabled_idx" ON "Product"("vipEnabled"); + +-- AlterTable +ALTER TABLE "ProductVariant" ADD COLUMN "vipPrice" INTEGER; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8fc1dc6..7268234 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -185,18 +185,24 @@ model Product { imageKey String? galleryUrls String[] @default([]) galleryKeys String[] @default([]) - tags String[] @default([]) - published Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - variants ProductVariant[] - orderItems OrderItem[] + tags String[] @default([]) + published Boolean @default(true) + vipEnabled Boolean @default(false) + vipOnly Boolean @default(false) + vipSpecialPrice Boolean @default(false) + vipPrice Int? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + variants ProductVariant[] + orderItems OrderItem[] @@index([storeId]) @@index([categoryId]) @@index([brandId]) @@index([price]) @@index([published]) + @@index([vipEnabled]) + @@index([vipOnly]) } model ProductVariant { @@ -208,6 +214,7 @@ model ProductVariant { packageId String @default("") price Int @default(0) discountPrice Int? + vipPrice Int? @@index([productId]) } diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 11406dc..7bf5b81 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -1,8 +1,9 @@ -import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common'; +import { Body, Controller, Get, Patch, Post, UseGuards } from '@nestjs/common'; import type { AuthUser } from './auth.types'; import { AuthService } from './auth.service'; import { CurrentUser } from './decorators/current-user.decorator'; import { CellNumberDto } from './dto/cell-number.dto'; +import { ChangePasswordDto } from './dto/change-password.dto'; import { ForgotPasswordResetDto } from './dto/forgot-password-reset.dto'; import { LoginDto } from './dto/login.dto'; import { OtpVerifyDto } from './dto/otp-verify.dto'; @@ -70,4 +71,13 @@ export class AuthController { me(@CurrentUser() user: AuthUser) { return this.auth.me(user); } + + @Patch('password') + @UseGuards(JwtAuthGuard) + changePassword( + @CurrentUser() user: AuthUser, + @Body() dto: ChangePasswordDto, + ) { + return this.auth.changePassword(user, dto); + } } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index eca73ee..4259fe6 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -14,6 +14,7 @@ import { PrismaService } from '../prisma/prisma.service'; import { SmsService } from '../sms/sms.service'; import { AuthUser, displayName } from './auth.types'; import { CellNumberDto } from './dto/cell-number.dto'; +import { ChangePasswordDto } from './dto/change-password.dto'; import { ForgotPasswordResetDto } from './dto/forgot-password-reset.dto'; import { LoginDto } from './dto/login.dto'; import { OtpVerifyDto } from './dto/otp-verify.dto'; @@ -276,6 +277,15 @@ export class AuthService { }; } + async changePassword(actor: AuthUser, dto: ChangePasswordDto) { + const passwordHash = await bcrypt.hash(dto.password, 10); + await this.prisma.user.update({ + where: { id: actor.id }, + data: { passwordHash }, + }); + return { ok: true as const }; + } + /** Expose OTP helpers for store registration flow. */ createAndSendOtpPublic(input: { cellNumber: string; diff --git a/src/auth/dto/change-password.dto.ts b/src/auth/dto/change-password.dto.ts new file mode 100644 index 0000000..f33ae0a --- /dev/null +++ b/src/auth/dto/change-password.dto.ts @@ -0,0 +1,7 @@ +import { IsString, MinLength } from 'class-validator'; + +export class ChangePasswordDto { + @IsString() + @MinLength(4, { message: 'رمز عبور باید حداقل ۴ کاراکتر باشد' }) + password!: string; +} diff --git a/src/products/dto/create-product.dto.ts b/src/products/dto/create-product.dto.ts index 0785f5f..1a8da7b 100644 --- a/src/products/dto/create-product.dto.ts +++ b/src/products/dto/create-product.dto.ts @@ -35,6 +35,11 @@ export class ProductVariantDto { @IsInt() @Min(0) discountPrice?: number; + + @IsOptional() + @IsInt() + @Min(0) + vipPrice?: number; } export class CreateProductDto { @@ -98,6 +103,23 @@ export class CreateProductDto { @IsBoolean() published?: boolean; + @IsOptional() + @IsBoolean() + vipEnabled?: boolean; + + @IsOptional() + @IsBoolean() + vipOnly?: boolean; + + @IsOptional() + @IsBoolean() + vipSpecialPrice?: boolean; + + @IsOptional() + @IsInt() + @Min(0) + vipPrice?: number; + @IsOptional() @IsArray() @ValidateNested({ each: true }) @@ -173,6 +195,23 @@ export class UpdateProductDto { @IsBoolean() published?: boolean; + @IsOptional() + @IsBoolean() + vipEnabled?: boolean; + + @IsOptional() + @IsBoolean() + vipOnly?: boolean; + + @IsOptional() + @IsBoolean() + vipSpecialPrice?: boolean; + + @IsOptional() + @IsInt() + @Min(0) + vipPrice?: number | null; + @IsOptional() @IsArray() @ValidateNested({ each: true }) diff --git a/src/products/products.service.ts b/src/products/products.service.ts index 4b2bb3f..03dcda7 100644 --- a/src/products/products.service.ts +++ b/src/products/products.service.ts @@ -97,6 +97,10 @@ export class ProductsService { galleryKeys: dto.galleryKeys ?? [], tags: dto.tags ?? [], published: dto.published ?? true, + vipEnabled: dto.vipEnabled ?? false, + vipOnly: dto.vipOnly ?? false, + vipSpecialPrice: dto.vipSpecialPrice ?? false, + vipPrice: dto.vipPrice, variants: dto.variants?.length ? { create: dto.variants.map((v) => ({ @@ -105,6 +109,7 @@ export class ProductsService { packageId: v.packageId ?? '', price: v.price, discountPrice: v.discountPrice, + vipPrice: v.vipPrice, })), } : undefined, @@ -132,6 +137,7 @@ export class ProductsService { packageId: v.packageId ?? '', price: v.price, discountPrice: v.discountPrice, + vipPrice: v.vipPrice, })), }); } @@ -164,6 +170,14 @@ export class ProductsService { : {}), ...(dto.tags !== undefined ? { tags: dto.tags } : {}), ...(dto.published !== undefined ? { published: dto.published } : {}), + ...(dto.vipEnabled !== undefined + ? { vipEnabled: dto.vipEnabled } + : {}), + ...(dto.vipOnly !== undefined ? { vipOnly: dto.vipOnly } : {}), + ...(dto.vipSpecialPrice !== undefined + ? { vipSpecialPrice: dto.vipSpecialPrice } + : {}), + ...(dto.vipPrice !== undefined ? { vipPrice: dto.vipPrice } : {}), }, include: { variants: true }, });