feat: باقي المجموعة I — أمان السحب (OTP + بصمة + HMAC + تدقيق)
I4 — OTP على السحب عبر نبيه. تدفّق خطوتين، وقاعدته: **لا يتحرك مال قبل إثبات الهوية**: - POST /payouts/request → يرسل رمزاً، حالة pending_otp، **بلا خصم** - POST /payouts/:id/confirm → يتحقق ثم يحجز ذرّياً (خصم I1) - حدّ 5 محاولات (رمز 4 خانات يُخمَّن في دقائق بلا حدّ)، الرمز يُستهلك مرة - complete يرفض طلباً بلا otp_verified_at (حارس ضد تخطّي التأكيد) - fail لا يردّ مالاً لطلب pending_otp — لم يُخصم منه شيء، وردّه يخلق مالاً I5 — biometric_method/at + device_id + request_ip على السحب. أثرٌ للتحقيق لا مصادقة: العميل يستطيع ادّعاءها، والمصادقة الحقيقية JWT + رمز واتساب. I6 — HMAC **مبنيّ ومطفأ** (PAYMENTS_REQUIRE_SIGNATURE=false) حتى يوقّع فلاتر؛ تفعيله الآن يقطع كل سحب. مفتاح **لكل جلسة** يُصدره الدخول لا سرّ ثابت في التطبيق (الثابت يُستخرج بالهندسة العكسية فيصير التوقيع مسرحية). يوقّع timestamp.METHOD.path.body بنافذة 5 دقائق؛ rawBody مفعّل في main. I7 — tripz_audit_log append-only: من·ماذا·متى·أي IP وجهاز. لا يرمي أبداً — فشل التدقيق يجب ألّا يُسقط عمليةً مالية نجحت. I2 أُلغيت الحاجة إليها: إيراد المنصة = الشحن، و credit_txns دفتره فعلاً؛ محفظة ثانية = دفتر مزدوج يحتاج مطابقة. الباقي تقرير لا محفظة. I3/I8 مؤجَّلتان بوعي (توثيق في docs/17). wallet-race-test.mjs حُدِّث: السباق انتقل من request إلى confirm، وأُضيف محكّ أن الطلب وحده لا يمسّ الرصيد. هجرة: PayoutSecurityAndAudit (تعتبر السحوبات القائمة مُتحقَّقة وإلا رفض complete صرفها للأبد). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
91e1692457
commit
1605754722
@@ -3,6 +3,7 @@ import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import Redis from 'ioredis';
|
||||
import { REDIS } from '../../common/redis/redis.module';
|
||||
import { SigningService } from '../../common/signing/signing.service';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { User } from '../users/entities/user.entity';
|
||||
import { TenantsService } from '../tenants/tenants.service';
|
||||
@@ -19,6 +20,7 @@ export class AuthService {
|
||||
private config: ConfigService,
|
||||
private tenantsService: TenantsService,
|
||||
private nabeh: NabehService,
|
||||
private readonly signing: SigningService,
|
||||
@Inject(REDIS) private readonly redis: Redis,
|
||||
) {}
|
||||
|
||||
@@ -107,7 +109,7 @@ export class AuthService {
|
||||
return this.issueTokens(user, user.tenant_id);
|
||||
}
|
||||
|
||||
private issueTokens(user: User, tenantId: string) {
|
||||
private async issueTokens(user: User, tenantId: string) {
|
||||
const base = {
|
||||
sub: user.id,
|
||||
phone: user.phone,
|
||||
@@ -120,6 +122,14 @@ export class AuthService {
|
||||
{ ...base, type: 'refresh' },
|
||||
{ expiresIn: (this.config.get<string>('jwt.refreshExpires') ?? '30d') as any },
|
||||
),
|
||||
// مفتاح توقيع العمليات المالية (docs/17 — I6). يُسلَّم مرة واحدة عند
|
||||
// الدخول ويُخزَّن في flutter_secure_storage.
|
||||
//
|
||||
// **لماذا مفتاح لكل جلسة لا سرّ ثابت في التطبيق؟** أي سرّ داخل التطبيق
|
||||
// يُستخرج بالهندسة العكسية فيصير التوقيع مسرحية. المفتاح هنا يُولَّد على
|
||||
// السيرفر لكل دخول، فمن يفكّك الـAPK لا يجد شيئاً، ومن يسرق توكناً
|
||||
// (الـAPI على http حالياً) لا يملك المفتاح فلا يستطيع توقيع سحب.
|
||||
signing_key: await this.signing.issue(tenantId, user.id),
|
||||
user,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user