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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
aae01fc4ee
commit
91e1692457
@@ -0,0 +1,50 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user