fix: D4 — حدّ الطلبات كان مُعطَّلاً كلياً رغم أنه يبدو مفعَّلاً
ThrottlerModule.forRoot(...) كان مسجَّلاً في app.module.ts منذ البداية — بلا أي حارس يطبّقه (لا APP_GUARD ولا @UseGuards). التسجيل وحده لا يفعل شيئاً في NestJS: كل نقطة في الـAPI، بما فيها verify-otp و payouts/*، كانت بلا أي حدّ طلبات إطلاقاً منذ اليوم الأول. - APP_GUARD → ThrottlerGuard يُفعّل الحدّ العام (120/دقيقة) على كل نقطة - عدّاد محاولات لكل (مستأجر، رقم) في AuthService.verifyOtp — الحماية الحقيقية ضد تخمين الرمز لا الحدّ العام: رمز 4 خانات = 10000 احتمال، ومهاجم يدوّر IP يتجاوز أي حدّ بالـIP وحده. 5 محاولات ثم إبطال الرمز، نفس نمط payouts.service (I4) - حدود أضيق للأهداف عالية القيمة: send-otp (كل إرسال يكلّف رسالة واتساب مدفوعة فعلياً)، verify-otp، payouts/request، payouts/confirm - HealthController مُستثنى (@SkipThrottle) — مراقبة تشغيل بلا بيانات حساسة قرار وعي بالمخاطرة: بنيت أولاً حارساً مخصَّصاً يتتبّع بالمستخدم المصادَق لا بالـIP وحده (مهم لموبايل — NAT عند مشغّلي الجوّال يجمع آلاف المستخدمين خلف IP واحد)، لكن تطلّب حقناً يدوياً دقيقاً (InjectThrottlerOptions/ InjectThrottlerStorage) — خطأ فيه يمنع إقلاع التطبيق كاملاً، ولا بيئة هنا لاختبار NestFactory.create() قبل الدفع (jest ينشئ الخدمات يدوياً فلا يكشف أخطاء DI لحارس عالمي). حذفته ورجّحت الأمان بالتتبّع الافتراضي — موثّق في docs/17 D4 لتُنفَّذ حين يمكن اختبارها فعلياً على السيرفر. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
253fbb4b2a
commit
c258a8c4b8
@@ -42,6 +42,16 @@ export class AuthService {
|
||||
return `otp:${tenantId}:${phone}`;
|
||||
}
|
||||
|
||||
private otpAttemptsKey(tenantId: string, phone: string): string {
|
||||
return `otp:attempts:${tenantId}:${phone}`;
|
||||
}
|
||||
|
||||
// حدّ محاولات لكل (مستأجر، رقم) — لا لكل IP (docs/17 — D4). حارس الطلبات
|
||||
// العام (ThrottlerGuard) يُبطئ مهاجماً واحداً من عنوان واحد؛ هذا يمنعه حتى
|
||||
// لو دوّر عناوين IP، لأن رمزاً من 4 خانات = 10000 احتمال يُخمَّن في دقائق
|
||||
// بلا هذا الحدّ. نفس النمط المستعمل في payouts.service (I4).
|
||||
private readonly OTP_MAX_ATTEMPTS = 5;
|
||||
|
||||
private genCode(): string {
|
||||
if (this.devMode) return '1234';
|
||||
const len = this.config.get<number>('auth.otpLength') ?? 4;
|
||||
@@ -80,11 +90,23 @@ export class AuthService {
|
||||
// في وضع التطوير: الرمز الثابت 1234 يمرّ دائماً (تسهيل الاختبار).
|
||||
const devBypass = this.devMode && code === '1234';
|
||||
if (!devBypass) {
|
||||
const attemptsKey = this.otpAttemptsKey(tenant.id, canonical);
|
||||
const attempts = await this.redis.incr(attemptsKey);
|
||||
if (attempts === 1) {
|
||||
// نفس عمر الرمز — لا داعي لعدّاد يبقى بعد انتهاء صلاحية الرمز نفسه.
|
||||
await this.redis.expire(attemptsKey, this.config.get<number>('auth.otpTtl') ?? 300);
|
||||
}
|
||||
if (attempts > this.OTP_MAX_ATTEMPTS) {
|
||||
await this.redis.del(this.otpKey(tenant.id, canonical)); // إبطال الرمز فوراً
|
||||
throw new UnauthorizedException('Too many attempts — request a new code');
|
||||
}
|
||||
|
||||
const stored = await this.redis.get(this.otpKey(tenant.id, canonical));
|
||||
if (!stored || stored !== code) {
|
||||
throw new UnauthorizedException('Invalid or expired OTP code');
|
||||
}
|
||||
await this.redis.del(this.otpKey(tenant.id, canonical));
|
||||
// نجاح — يُستهلك الرمز والعدّاد معاً؛ لا فائدة من عدّاد بعد رمز صحيح.
|
||||
await this.redis.del(this.otpKey(tenant.id, canonical), attemptsKey);
|
||||
}
|
||||
|
||||
let user = await this.usersService.findByPhone(tenant.id, canonical);
|
||||
|
||||
Reference in New Issue
Block a user