Add SMS OTP, harden auth, and fix order/checkout server bugs.

Ship 4-digit OTP with rate-limit attempts, Tehran-safe expiry/rass math, larger JSON body limit for check images, and safer order pricing/status guards. Include rebuilt dist for prebuilt deploy.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-08 14:55:26 +03:30
co-authored by Cursor
parent b121797263
commit 345a4a4708
100 changed files with 1476 additions and 353 deletions
+62 -17
View File
@@ -46,24 +46,31 @@ exports.AuthService = void 0;
const common_1 = require("@nestjs/common");
const config_1 = require("@nestjs/config");
const jwt_1 = require("@nestjs/jwt");
const node_crypto_1 = require("node:crypto");
const bcrypt = __importStar(require("bcryptjs"));
const prisma_service_1 = require("../prisma/prisma.service");
const sms_service_1 = require("../sms/sms.service");
const phone_1 = require("../common/utils/phone");
const MAX_OTP_ATTEMPTS = 5;
let AuthService = class AuthService {
prisma;
jwt;
config;
constructor(prisma, jwt, config) {
sms;
constructor(prisma, jwt, config, sms) {
this.prisma = prisma;
this.jwt = jwt;
this.config = config;
this.sms = sms;
}
async requestOtp(dto) {
const phone = (0, phone_1.normalizePhone)((0, phone_1.toEnglishDigits)(dto.phone));
if (!(0, phone_1.isValidPhone)(phone)) {
throw new common_1.BadRequestException('شماره موبایل نامعتبر است');
}
const existing = await this.prisma.customer.findUnique({ where: { phone } });
const existing = await this.prisma.customer.findUnique({
where: { phone },
});
const isRegister = Boolean(dto.isRegister);
if (isRegister) {
if (!dto.firstName?.trim() || !dto.lastName?.trim()) {
@@ -76,27 +83,49 @@ let AuthService = class AuthService {
else if (!existing) {
throw new common_1.BadRequestException('کاربری با این شماره یافت نشد');
}
const code = this.config.get('MOCK_OTP_CODE', '123456');
else if (!existing.isActive) {
throw new common_1.UnauthorizedException('حساب کاربری شما غیرفعال است');
}
const smsEnabled = this.sms.isEnabled();
const code = smsEnabled
? String((0, node_crypto_1.randomInt)(1000, 10000))
: this.config.get('MOCK_OTP_CODE', '1234');
const ttl = Number(this.config.get('OTP_TTL_MINUTES', 5));
const expiresAt = new Date(Date.now() + ttl * 60_000);
await this.prisma.otpChallenge.deleteMany({ where: { phone } });
await this.prisma.otpChallenge.create({
data: {
phone,
code,
firstName: dto.firstName?.trim(),
lastName: dto.lastName?.trim(),
isRegister,
expiresAt,
},
const challenge = await this.prisma.$transaction(async (tx) => {
await tx.otpChallenge.deleteMany({ where: { phone } });
return tx.otpChallenge.create({
data: {
phone,
code,
firstName: dto.firstName?.trim(),
lastName: dto.lastName?.trim(),
isRegister,
expiresAt,
},
});
});
if (smsEnabled) {
const sent = await this.sms.send(phone, this.sms.buildOtpMessage(code));
if (!sent.success) {
await this.prisma.otpChallenge.deleteMany({
where: { id: challenge.id },
});
throw new common_1.ServiceUnavailableException('ارسال پیامک با خطا مواجه شد؛ دوباره تلاش کنید');
}
}
return {
success: true,
message: 'کد تأیید ارسال شد',
expiresAt,
debugCode: code,
...(this.exposesDebugCode(smsEnabled) ? { debugCode: code } : {}),
};
}
exposesDebugCode(smsEnabled) {
if (smsEnabled)
return false;
return this.config.get('NODE_ENV') !== 'production';
}
async verifyOtp(dto) {
const phone = (0, phone_1.normalizePhone)((0, phone_1.toEnglishDigits)(dto.phone));
const code = (0, phone_1.toEnglishDigits)(dto.code).trim();
@@ -107,7 +136,15 @@ let AuthService = class AuthService {
if (!challenge || challenge.expiresAt < new Date()) {
throw new common_1.UnauthorizedException('کد منقضی شده است');
}
if (challenge.attempts >= MAX_OTP_ATTEMPTS) {
await this.prisma.otpChallenge.deleteMany({ where: { phone } });
throw new common_1.UnauthorizedException('تعداد تلاش‌های نادرست زیاد است؛ کد جدید بگیرید');
}
if (challenge.code !== code) {
await this.prisma.otpChallenge.update({
where: { id: challenge.id },
data: { attempts: { increment: 1 } },
});
throw new common_1.UnauthorizedException('کد تأیید نادرست است');
}
let user = await this.prisma.customer.findUnique({ where: { phone } });
@@ -126,6 +163,9 @@ let AuthService = class AuthService {
else if (!user) {
throw new common_1.UnauthorizedException('کاربر یافت نشد');
}
else if (!user.isActive) {
throw new common_1.UnauthorizedException('حساب کاربری شما غیرفعال است');
}
await this.prisma.otpChallenge.deleteMany({ where: { phone } });
const accessToken = await this.signCustomerToken(user.id, user.phone);
return {
@@ -166,7 +206,9 @@ let AuthService = class AuthService {
}
async me(userId, type) {
if (type === 'admin') {
const admin = await this.prisma.adminUser.findUnique({ where: { id: userId } });
const admin = await this.prisma.adminUser.findUnique({
where: { id: userId },
});
if (!admin)
throw new common_1.UnauthorizedException();
return {
@@ -177,7 +219,9 @@ let AuthService = class AuthService {
role: admin.role,
};
}
const user = await this.prisma.customer.findUnique({ where: { id: userId } });
const user = await this.prisma.customer.findUnique({
where: { id: userId },
});
if (!user)
throw new common_1.UnauthorizedException();
return this.mapCustomer(user);
@@ -216,6 +260,7 @@ exports.AuthService = AuthService = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [prisma_service_1.PrismaService,
jwt_1.JwtService,
config_1.ConfigService])
config_1.ConfigService,
sms_service_1.SmsService])
], AuthService);
//# sourceMappingURL=auth.service.js.map