Files
tripz-llc/backend/src/common/platform/platform.guard.ts
T
Hamza-AyedandClaude Opus 4.8 91e1692457 feat: حارس السوبر-أدمن + سياسة الدين لكل مستأجر
PlatformGuard (x-platform-secret، مقارنة ثابتة الزمن) يحرس admin/tenants*
و admin/features و platform/*. بدونه كان أدمن أي مستأجر يرقّي اشتراكه بنفسه
— أي أن المجموعة K كلها كانت زينة. سرّ غير مضبوط = منع الجميع لا فتح الباب
(الإعداد الناقص خطأ تشغيلي شائع، وفتح الباب عنده أسوأ من إغلاقه).
admin/users/:id/role تبقى لأدمن المستأجر: نطاقها من التوكن، وليست نقطة منصة.

سياسة الدين لكل مستأجر (قرار المالك): كل مشغّل يحدّد كم ديناً يسمح به،
و0 خيار صريح = لا دين إطلاقاً.
- tenants.settings jsonb — سياسات تشغيل يحكمها المستأجر، منفصلة عن features
  (استحقاقات يحكمها السوبر-أدمن): «كيف تشتغل» مقابل «ماذا اشتريت»
- credit-policy.ts: debt_allowance + signup_bonus لكل عملة. الافتراضات
  صُحّحت لقيم المالك: 2 JOD · 200 SYP · 200 EGP (كانت 5/500/500)
- تُخزَّن موجبةً («كم ديناً أسمح؟») لا كأرضية سالبة — أوضح للمشغّل و0 بلا
  لبس في الإشارة
- PATCH /credit/policy لأدمن المستأجر؛ updateSettings يدمج عميقاً فلا يمحو
  سياسة أخرى، ويُبطل الكاش وإلا سرت السياسة القديمة حتى ساعة
- إعداد مكسور يسقط للافتراض لا لدين بلا حدّ. والصفر لا يُبتلع (فخّ ||)

هجرة: TenantSettings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 15:26:41 +03:00

51 lines
2.1 KiB
TypeScript

import {
CanActivate,
ExecutionContext,
Injectable,
Logger,
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { timingSafeEqual } from 'crypto';
/**
* حارس السوبر-أدمن (مالك المنصة) — يحرس `admin/*` و`platform/*`.
*
* السوبر-أدمن يدير **كل** المستأجرين، فهو خارج نموذج أدوار المستأجر تماماً
* (rider/driver/dispatcher/admin): أدمن المستأجر «سيرو» يجب ألّا يرقّي اشتراكه
* بنفسه. لذلك المصادقة بسرّ المنصة لا بـJWT مستأجر.
*
* مؤقّت حتى يوجد نموذج مستخدم منصة حقيقي؛ يوثَّق في docs/19.
*/
@Injectable()
export class PlatformGuard implements CanActivate {
private readonly logger = new Logger('PlatformGuard');
constructor(private readonly config: ConfigService) {}
canActivate(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest();
const provided = req.headers?.['x-platform-secret'];
const expected = this.config.get<string>('platform.secret');
// سرّ غير مضبوط = **منع الجميع**، لا سماح للجميع. تركه فارغاً في الإنتاج
// خطأ تشغيلي شائع، وفتحُ الباب عنده أسوأ من إغلاقه.
if (!expected) {
this.logger.error('PLATFORM_SECRET is not set — refusing all platform access');
throw new UnauthorizedException('platform access disabled');
}
if (typeof provided !== 'string' || !PlatformGuard.safeEqual(provided, expected)) {
throw new UnauthorizedException('invalid platform secret');
}
return true;
}
/** مقارنة ثابتة الزمن — المقارنة العادية تسرّب السرّ حرفاً حرفاً بالتوقيت. */
private static safeEqual(a: string, b: string): boolean {
const ba = Buffer.from(a);
const bb = Buffer.from(b);
if (ba.length !== bb.length) return false;
return timingSafeEqual(ba, bb);
}
}