feat: D5 — OTP متعدد المزوّدين حسب الدولة (من سيرو) + قرار بلا كلمة مرور
مراجعة سيرو (auth/otp/): مصر تستعمل Kazumi SMS مع failover لواتساب، وسوريا/الأردن Nabeh. عندنا كان مزوّد واحد فقط. - integrations/otp/: واجهة OtpProvider + OtpDispatcher يوجّه حسب tenant.countryPack بسلسلة failover (eg: [kazumi, nabeh] · jo/sy: [nabeh]) - KazumiSmsProvider (مصر SMS) + NabehOtpProvider (غلاف على NabehService) - كل مزوّد يرسل رمزاً نولّده نحن (Redis) — استبعدنا نمط Intaleq (يولّد الرمز بنفسه) حفاظاً على مصدر واحد للرمز - موحَّد: AuthService.sendOtp و PayoutsService.issueOtp يمرّان بنفس المُوزِّع الآن — أُزيل كل استدعاء Nabeh مباشر. أي مسار يحتاج رسالة يستدعي الخدمة الموحّدة (طلب المالك) - فشل السلسلة كاملة = ServiceUnavailable صريح، لا صمت قرار المالك (2026-07-17): بلا كلمة مرور إطلاقاً. المصادقة = هاتف + OTP مرة → جلسة مربوطة بالجهاز (D2). التسجيل بالهاتف لا Google/Apple (محظوران في بعض الدول). سيرو نفسه لا كلمة مرور حقيقية له (password = hash(email) وهمي، ومقارنة نصّية غير آمنة في مسار التجربة). يطابق أوبر/كريم/inDrive. مراجعة حدّ الطلبات في سيرو (RateLimiter.php): حدود مسمّاة لكل نوع + fallback بملف عند تعطّل Redis (fail-closed). عندنا مسار OTP fail-closed أصلاً؛ تخزين Throttler على Redis مؤجَّل للتوسّع الأفقي الفعلي (موثّق في D4). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c258a8c4b8
commit
8f6c46f0ac
@@ -1,4 +1,10 @@
|
||||
import { Inject, Injectable, Logger, UnauthorizedException } from '@nestjs/common';
|
||||
import {
|
||||
Inject,
|
||||
Injectable,
|
||||
Logger,
|
||||
ServiceUnavailableException,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import Redis from 'ioredis';
|
||||
@@ -8,7 +14,7 @@ import { UsersService } from '../users/users.service';
|
||||
import { User } from '../users/entities/user.entity';
|
||||
import { TenantsService } from '../tenants/tenants.service';
|
||||
import { Tenant } from '../../database/entities/tenant.entity';
|
||||
import { NabehService } from '../../integrations/nabeh/nabeh.service';
|
||||
import { OtpDispatcher } from '../../integrations/otp/otp-dispatcher.service';
|
||||
import { PhoneService } from '../../common/phone/phone.service';
|
||||
import { DeviceService } from '../../common/device/device.service';
|
||||
|
||||
@@ -21,7 +27,7 @@ export class AuthService {
|
||||
private jwtService: JwtService,
|
||||
private config: ConfigService,
|
||||
private tenantsService: TenantsService,
|
||||
private nabeh: NabehService,
|
||||
private readonly otp: OtpDispatcher,
|
||||
private readonly signing: SigningService,
|
||||
private readonly phones: PhoneService,
|
||||
private readonly device: DeviceService,
|
||||
@@ -76,9 +82,15 @@ export class AuthService {
|
||||
return { success: true, message: 'OTP sent (dev)', dev_code: code };
|
||||
}
|
||||
|
||||
// إرسال حقيقي عبر واتساب (Nabeh)
|
||||
await this.nabeh.sendOtp(this.phones.toWhatsApp(canonical), code);
|
||||
return { success: true, message: 'OTP sent via WhatsApp' };
|
||||
// إرسال حقيقي — المُوزِّع يختار المزوّد حسب دولة المستأجر مع failover
|
||||
// (docs/17 — D5). فشل السلسلة كاملة = لا رمز يصل، فنُفشل الطلب صراحةً.
|
||||
const sent = await this.otp.send(this.phones.toWhatsApp(canonical), code, {
|
||||
countryPack: tenant.countryPack,
|
||||
});
|
||||
if (!sent) {
|
||||
throw new ServiceUnavailableException('Failed to send verification code — please try again');
|
||||
}
|
||||
return { success: true, message: 'OTP sent' };
|
||||
}
|
||||
|
||||
async verifyOtp(tenantSlug: string, phone: string, code: string, deviceId?: string) {
|
||||
|
||||
@@ -8,10 +8,11 @@ import { PaymentsController } from './payments.controller';
|
||||
import { PayoutsController } from './payouts.controller';
|
||||
import { WalletModule } from '../wallet/wallet.module';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import { TenantsModule } from '../tenants/tenants.module';
|
||||
|
||||
@Module({
|
||||
// NabehModule و AuditModule عالميان.
|
||||
imports: [TypeOrmModule.forFeature([Payment, Payout]), WalletModule, UsersModule],
|
||||
// OtpModule و AuditModule عالميان.
|
||||
imports: [TypeOrmModule.forFeature([Payment, Payout]), WalletModule, UsersModule, TenantsModule],
|
||||
controllers: [PaymentsController, PayoutsController],
|
||||
providers: [PaymentsService, PayoutsService],
|
||||
exports: [PaymentsService, PayoutsService],
|
||||
|
||||
@@ -16,7 +16,8 @@ import { REDIS } from '../../common/redis/redis.module';
|
||||
import { Payout } from './entities/payout.entity';
|
||||
import { WalletService } from '../wallet/wallet.service';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { NabehService } from '../../integrations/nabeh/nabeh.service';
|
||||
import { OtpDispatcher } from '../../integrations/otp/otp-dispatcher.service';
|
||||
import { TenantsService } from '../tenants/tenants.service';
|
||||
import { AuditService } from '../../common/audit/audit.service';
|
||||
|
||||
export interface PayoutRequestDto {
|
||||
@@ -52,7 +53,8 @@ export class PayoutsService {
|
||||
@Inject(REDIS) private readonly redis: Redis,
|
||||
private readonly wallet: WalletService,
|
||||
private readonly users: UsersService,
|
||||
private readonly nabeh: NabehService,
|
||||
private readonly tenants: TenantsService,
|
||||
private readonly otp: OtpDispatcher,
|
||||
private readonly audit: AuditService,
|
||||
private readonly config: ConfigService,
|
||||
) {}
|
||||
@@ -253,8 +255,14 @@ export class PayoutsService {
|
||||
}
|
||||
const user = await this.users.findById(tenantId, driverUserId);
|
||||
if (!user?.phone) throw new BadRequestException('no phone on file');
|
||||
// فشل الإرسال يُفشل الطلب: طلبٌ بلا رمز يصل = سائق عالق بلا سبيل للتأكيد.
|
||||
await this.nabeh.sendOtp(user.phone, code);
|
||||
// نفس مُوزِّع OTP الموحّد المستعمل في تسجيل الدخول (docs/17 — D5): يوجّه
|
||||
// حسب دولة المستأجر ويطبّق failover. فشل السلسلة = سائق عالق بلا رمز.
|
||||
const tenant = await this.tenants.resolve(tenantId);
|
||||
const sent = await this.otp.send(user.phone, code, {
|
||||
countryPack: tenant?.countryPack ?? 'jo',
|
||||
userType: 'driver',
|
||||
});
|
||||
if (!sent) throw new BadRequestException('failed to send verification code');
|
||||
return code;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user