update package-lock.json 050224

This commit is contained in:
2026-05-18 14:37:02 +03:30
parent 5fc120becd
commit 97abb808b8
6 changed files with 3645 additions and 1435 deletions
+7
View File
@@ -9,9 +9,16 @@ DB_SYNC=true
JWT_SECRET=change_me_to_a_long_random_secret
OTP_HASH_SECRET=change_me_to_a_long_random_secret
SUPER_ADMIN_MOBILE=09123456789
# Min seconds between OTP requests for the same mobile
OTP_REQUEST_COOLDOWN_SECONDS=60
# Max OTP requests per mobile per rolling hour
OTP_REQUEST_MAX_PER_HOUR=5
# Dev/test only: returns OTP code in API response instead of sending SMS
AUTH_RETURN_OTP=true
# Comma-separated frontend origins (required in production for browser clients)
# CORS_ORIGINS=https://app.example.com,http://localhost:5173
# 0098sms.com — used when AUTH_RETURN_OTP is not true
# From panel: account username, password (or web service password), approved sender line (FROM)
SMS_USERNAME=
+2
View File
@@ -0,0 +1,2 @@
registry=https://mirror-npm.runflare.com
strict-ssl=false
+3530 -1432
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -4,8 +4,21 @@ import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';
function configureCors(app: Awaited<ReturnType<typeof NestFactory.create>>) {
const raw = process.env.CORS_ORIGINS?.trim();
if (raw) {
const origins = raw.split(',').map((o) => o.trim()).filter(Boolean);
app.enableCors({ origin: origins, credentials: true });
return;
}
if (process.env.NODE_ENV !== 'production') {
app.enableCors({ origin: true, credentials: true });
}
}
async function bootstrap() {
const app = await NestFactory.create(AppModule);
configureCors(app);
app.useGlobalPipes(
new ValidationPipe({
forbidNonWhitelisted: true,
+59
View File
@@ -1,6 +1,9 @@
import {
BadRequestException,
HttpException,
HttpStatus,
Injectable,
Logger,
ServiceUnavailableException,
UnauthorizedException,
} from '@nestjs/common';
@@ -28,8 +31,11 @@ type VerifyOtpResponse = {
@Injectable()
export class AuthService {
private readonly logger = new Logger(AuthService.name);
private readonly otpTtlSeconds: number;
private readonly maxAttempts: number;
private readonly otpRequestCooldownSeconds: number;
private readonly otpRequestMaxPerHour: number;
constructor(
private readonly config: ConfigService,
@@ -42,10 +48,20 @@ export class AuthService {
) {
this.otpTtlSeconds = this.readPositiveInt('OTP_TTL_SECONDS', 120);
this.maxAttempts = this.readPositiveInt('OTP_MAX_ATTEMPTS', 5);
this.otpRequestCooldownSeconds = this.readPositiveInt(
'OTP_REQUEST_COOLDOWN_SECONDS',
60,
);
this.otpRequestMaxPerHour = this.readPositiveInt(
'OTP_REQUEST_MAX_PER_HOUR',
5,
);
}
async requestOtp(dto: RequestOtpDto): Promise<RequestOtpResponse> {
const mobile = this.normalizeMobile(dto.mobile);
await this.assertOtpRequestAllowed(mobile);
const code = this.generateOtpCode();
const expiresAt = new Date(Date.now() + this.otpTtlSeconds * 1000);
@@ -73,6 +89,9 @@ export class AuthService {
}
const sent = await this.sms.sendOtpSms(mobile, code);
if (!sent) {
this.logger.warn(
`OTP SMS failed for ${this.maskMobile(mobile)}`,
);
throw new ServiceUnavailableException(
'Could not send SMS. Please try again later.',
);
@@ -208,6 +227,46 @@ export class AuthService {
return configured ? this.normalizeMobile(configured) === mobile : false;
}
private async assertOtpRequestAllowed(mobile: string): Promise<void> {
const latest = await this.otps.findOne({
where: { mobile },
order: { createdAt: 'DESC' },
});
if (latest) {
const elapsedMs = Date.now() - latest.createdAt.getTime();
const cooldownMs = this.otpRequestCooldownSeconds * 1000;
if (elapsedMs < cooldownMs) {
const retryAfterSeconds = Math.ceil(
(cooldownMs - elapsedMs) / 1000,
);
throw new HttpException(
`Please wait ${retryAfterSeconds} seconds before requesting a new code`,
HttpStatus.TOO_MANY_REQUESTS,
);
}
}
const hourAgo = new Date(Date.now() - 60 * 60 * 1000);
const recentCount = await this.otps.count({
where: { mobile, createdAt: MoreThan(hourAgo) },
});
if (recentCount >= this.otpRequestMaxPerHour) {
throw new HttpException(
'Too many OTP requests. Please try again later',
HttpStatus.TOO_MANY_REQUESTS,
);
}
}
private maskMobile(mobile: string): string {
if (mobile.length <= 6) {
return '***';
}
return `${mobile.slice(0, 4)}****${mobile.slice(-2)}`;
}
private readPositiveInt(key: string, fallback: number): number {
const raw = this.config.get<string>(key);
const parsed = raw ? Number.parseInt(raw, 10) : NaN;
+34 -3
View File
@@ -62,25 +62,56 @@ export class SmsService {
const url = `${SMS_0098_SEND_URL}?${params.toString()}`;
const to = this.normalizePhoneForSms(phone);
try {
const res = await fetch(url);
const body = await res.text();
const body = (await res.text()).trim();
if (!res.ok) {
this.logger.warn(
`SMS HTTP ${res.status}: ${body.slice(0, 300)}`,
`SMS HTTP ${res.status} to=${this.maskPhone(to)} body=${body.slice(0, 300)}`,
);
return false;
}
if (!this.isLikelySuccessBody(body)) {
this.logger.warn(
`SMS provider rejected to=${this.maskPhone(to)} body=${body.slice(0, 300)}`,
);
return false;
}
return true;
} catch (e) {
this.logger.error(
'[SMS] request failed',
`SMS request failed to=${this.maskPhone(to)}`,
e instanceof Error ? e.stack : e,
);
return false;
}
}
/** 0098 often returns plain text; treat obvious error tokens as failure. */
private isLikelySuccessBody(body: string): boolean {
if (!body) {
return true;
}
const lower = body.toLowerCase();
if (
lower.includes('error') ||
lower.includes('fail') ||
lower.includes('invalid')
) {
return false;
}
return true;
}
private maskPhone(phone: string): string {
if (phone.length <= 6) {
return '***';
}
return `${phone.slice(0, 4)}****${phone.slice(-2)}`;
}
private getPassword(): string | undefined {
return (
this.config.get<string>('SMS_PASSWORD') ??