Add change-password endpoint and VIP product pricing fields.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-08 00:51:25 +03:30
co-authored by Cursor
parent fe78a99b78
commit 3da8a5d4a2
9 changed files with 110 additions and 8 deletions
+1 -1
View File
@@ -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
@@ -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");
@@ -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;
+13 -6
View File
@@ -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])
}
+11 -1
View File
@@ -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);
}
}
+10
View File
@@ -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;
+7
View File
@@ -0,0 +1,7 @@
import { IsString, MinLength } from 'class-validator';
export class ChangePasswordDto {
@IsString()
@MinLength(4, { message: 'رمز عبور باید حداقل ۴ کاراکتر باشد' })
password!: string;
}
+39
View File
@@ -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 })
+14
View File
@@ -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 },
});