feat: المجموعة E (كشف الاحتيال) + تشفير الهاتف at-rest
E — اتصال ثم إلغاء (من driver_ride_scam عند سيرو): - E1: driver_called_passenger + last_call_at/by/count على الرحلة نفسها لا Redis فقط — حقيقة دائمة تفيد التحقيق بعد انتهاء الرحلة وزوالها من الكاش. يُسجَّل عند call:offer (بدء المكالمة الفعلي) في RealtimeGateway عبر Repository<Trip> مباشرة — لا استيراد TripsModule كاملاً (يستورد RealtimeModule أصلاً، فدورة استيراد) - E2: FraudService.recordCallThenCancel — عدّاد يومي منفصل عن عدّاد الإلغاء العام بالساعة. 3/يوم = إنذار لا يمنع الإلغاء، 6/يوم = حظر مؤقت (طلب المالك حرفياً). يرتبط فقط إن كانت المكالمة خلال 30 دقيقة من الإلغاء تشفير الهاتف at-rest (ملاحظة المالك) — بفهرس أعمى منفصل، لا تشفير ساذج: - AES-256-GCM عشوائي IV يمنع WHERE phone = :x من المطابقة أصلاً (كل تشفير مختلف شكلاً حتى لنفس الرقم) — لو طُبِّق التشفير مباشرة بلا هذا لكسر تسجيل الدخول بالكامل بصمت - users.phone_bidx = HMAC-SHA256(الرقم المطبَّع) — حتمي، يصلح للبحث وللقيد الفريد. القيد انتقل من (tenant_id, phone) إلى (tenant_id, phone_bidx) — القديم كان سيقبل تكرار نفس الرقم الحقيقي فعلياً - findByPhone/create يحسبان البصمة؛ لا نقطة أخرى تقرأ phone بمساواة خام - هجرة EncryptPhone تُشفّر الصفوف القائمة فوراً وتبني البصمة بأثر رجعي — استثناء متعمَّد من تسامح decrypt() المعتاد (الهاتف عمود بحث يومي، خطره كنص صافٍ أعلى من name) - يعتمد على تطبيع D1: بصمة حتمية لنفس *النص*، فلا بد أن يصل الرقم مطبَّعاً قبلها وإلا عاد نفس الرقم بصيغتين ليصير رقمين مرة أخرى هجرات: TripCallSignal · EncryptPhone. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1eb79fee99
commit
253fbb4b2a
@@ -8,8 +8,11 @@ import {
|
||||
} from '@nestjs/websockets';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
import { DriversService } from '../modules/drivers/drivers.service';
|
||||
import { Trip } from '../modules/trips/entities/trip.entity';
|
||||
|
||||
/**
|
||||
* البوابة الحية (Socket.IO) — docs/09. مصادقة JWT + غرف مُنطَّقة بالمستأجر،
|
||||
@@ -25,6 +28,7 @@ export class RealtimeGateway implements OnGatewayConnection {
|
||||
constructor(
|
||||
private readonly jwt: JwtService,
|
||||
private readonly drivers: DriversService,
|
||||
@InjectRepository(Trip) private readonly trips: Repository<Trip>,
|
||||
) {}
|
||||
|
||||
handleConnection(client: Socket) {
|
||||
@@ -91,9 +95,34 @@ export class RealtimeGateway implements OnGatewayConnection {
|
||||
}
|
||||
|
||||
// ---- إشارات WebRTC (مكالمة مجانية بين الطرفين عبر غرفة الرحلة) ----
|
||||
/**
|
||||
* `offer` هو حدث **بدء الاتصال** فعلياً (answer/ice/end إشارات تفاوض
|
||||
* لاحقة على نفس المكالمة) — لذا هنا فقط نُسجّل إشارة الاحتيال (docs/17 —
|
||||
* E1). الكتابة على القاعدة مباشرة لا عبر Redis: تكرار المكالمات لا يقارن
|
||||
* بتكرار نبضات الموقع، فلا حاجة لتجميع (نمط H) هنا.
|
||||
*/
|
||||
@SubscribeMessage('call:offer')
|
||||
onCallOffer(@ConnectedSocket() c: Socket, @MessageBody() b: any) {
|
||||
async onCallOffer(@ConnectedSocket() c: Socket, @MessageBody() b: any) {
|
||||
this.relay(c, 'call:offer', b);
|
||||
const u = c.data.user;
|
||||
if (!u || !b?.tripId) return;
|
||||
try {
|
||||
await this.trips
|
||||
.createQueryBuilder()
|
||||
.update(Trip)
|
||||
.set({
|
||||
last_call_at: () => 'now()',
|
||||
last_call_by: u.role,
|
||||
call_count: () => 'call_count + 1',
|
||||
...(u.role === 'driver' ? { driver_called_passenger: true } : {}),
|
||||
})
|
||||
.where('tenant_id = :tenantId AND id = :tripId', { tenantId: u.tenantId, tripId: b.tripId })
|
||||
.execute();
|
||||
} catch (e: any) {
|
||||
// تسجيل الإشارة لا يجوز أن يقطع المكالمة نفسها — أُعيد المحاولة لاحقاً
|
||||
// غير ممكنة هنا، فنكتفي باللوغ.
|
||||
this.logger.warn(`call signal record failed: ${e?.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeMessage('call:answer')
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { RealtimeGateway } from './realtime.gateway';
|
||||
import { DriversModule } from '../modules/drivers/drivers.module';
|
||||
import { Trip } from '../modules/trips/entities/trip.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
DriversModule,
|
||||
// تسجيل الكيان مباشرة لا وحدة TripsModule كاملة — TripsModule تستورد
|
||||
// RealtimeModule أصلاً، فاستيراد معاكس يصنع دورة (docs/17 — E1).
|
||||
TypeOrmModule.forFeature([Trip]),
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
|
||||
Reference in New Issue
Block a user