From 82e74f86f0ea8d8fdb9ba30e8f6b46ef495567cc Mon Sep 17 00:00:00 2001 From: adonis Date: Wed, 9 Sep 2026 22:07:40 +0330 Subject: [PATCH] fix remote HMAC: match tenant domain only, not shared adminUrl k.montzar.ir on Gholaktala adminUrl was selecting tenant app.gholaktala.ir. HMAC now uses primary domain (and its subdomain), not admin/app/node URLs. Co-authored-by: Cursor --- src/auth/remote-sms-auth.guard.ts | 6 +++++- src/auth/tenant-host.util.spec.ts | 34 ++++++++++++++++++++++--------- src/auth/tenant-host.util.ts | 18 +++++++++++----- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/src/auth/remote-sms-auth.guard.ts b/src/auth/remote-sms-auth.guard.ts index 30e7683..fd065b1 100644 --- a/src/auth/remote-sms-auth.guard.ts +++ b/src/auth/remote-sms-auth.guard.ts @@ -160,7 +160,11 @@ export class RemoteSmsAuthGuard implements CanActivate { }, }); - const hit = findTenantForHost(tenants as TenantHostRow[], normalizedHost); + const hit = findTenantForHost( + tenants as TenantHostRow[], + normalizedHost, + 'hmac', + ); if (!hit) return null; return { diff --git a/src/auth/tenant-host.util.spec.ts b/src/auth/tenant-host.util.spec.ts index 650a57b..af83c49 100644 --- a/src/auth/tenant-host.util.spec.ts +++ b/src/auth/tenant-host.util.spec.ts @@ -1,10 +1,10 @@ import { findTenantForHost, scoreTenantHost } from './tenant-host.util'; const gholak = { - domain: 'gholaktala.ir', + domain: 'app.gholaktala.ir', domains: [], - appUrl: 'https://gholaktala.ir', - adminUrl: 'https://admin.gholaktala.ir', + appUrl: 'https://app.gholaktala.ir', + adminUrl: 'https://k.montzar.ir/admin', nodeServer: 'https://new.gholaktala.ir/server', }; @@ -25,22 +25,36 @@ const otherOnSameBox = { }; describe('findTenantForHost', () => { - const tenants = [montzar, otherOnSameBox, gholak]; + const tenants = [gholak, otherOnSameBox, montzar]; - it('maps new.gholaktala.ir to gholaktala, not the first shared nodeServer tenant', () => { - expect(findTenantForHost(tenants, 'new.gholaktala.ir')?.domain).toBe( - 'gholaktala.ir', + it('hmac does not treat shared adminUrl as gholaktala', () => { + expect(scoreTenantHost(gholak, 'k.montzar.ir', 'hmac')).toBe(0); + expect(findTenantForHost(tenants, 'k.montzar.ir', 'hmac')?.domain).toBe( + 'k.montzar.ir', ); - expect(scoreTenantHost(gholak, 'new.gholaktala.ir')).toBe(75); - expect(scoreTenantHost(montzar, 'new.gholaktala.ir')).toBe(10); }); - it('maps k.montzar.ir to montzar by primary domain', () => { + it('hmac without montzar tenant does not fall back to gholak adminUrl', () => { + expect( + findTenantForHost([gholak, otherOnSameBox], 'k.montzar.ir', 'hmac'), + ).toBeUndefined(); + }); + + it('full get_user still maps k.montzar.ir to montzar by primary domain', () => { expect(findTenantForHost(tenants, 'k.montzar.ir')?.domain).toBe( 'k.montzar.ir', ); }); + it('maps new.gholaktala.ir to gholaktala via subdomain, not nodeServer', () => { + const gholakBare = { ...gholak, domain: 'gholaktala.ir' }; + expect( + findTenantForHost([montzar, otherOnSameBox, gholakBare], 'new.gholaktala.ir') + ?.domain, + ).toBe('gholaktala.ir'); + expect(scoreTenantHost(gholakBare, 'new.gholaktala.ir')).toBe(75); + }); + it('does not pick a random tenant when only nodeServer matches several rows', () => { const sharedOnly = [ { ...montzar, domain: 'a.ir', domains: [] }, diff --git a/src/auth/tenant-host.util.ts b/src/auth/tenant-host.util.ts index 18195cc..1d2227e 100644 --- a/src/auth/tenant-host.util.ts +++ b/src/auth/tenant-host.util.ts @@ -62,14 +62,17 @@ function isSubdomainOf(host: string, parent: string): boolean { return child === root || child.endsWith('.' + root); } +export type TenantHostMatchMode = 'full' | 'hmac'; + /** - * Same identity as GET /get_user?domain= — primary domain, then aliases, - * then app/admin URLs. nodeServer is last and only if unique (many versions - * share one API host like new.gholaktala.ir). + * full: get_user — دامنه، alias، app/admin/node. + * hmac: فقط دامنهٔ اصلی نسخه (و ساب‌دامنهٔ همان). adminUrl مشترک + * (k.montzar.ir روی نسخهٔ app.gholaktala.ir) هویت نیست. */ export function scoreTenantHost( tenant: TenantHostFields, hostRaw: string, + mode: TenantHostMatchMode = 'full', ): number { const host = normalizeTenantHost(hostRaw); if (!host) return 0; @@ -77,6 +80,11 @@ export function scoreTenantHost( hostsEqual(normalizeTenantHost(raw), host); if (eq(tenant.domain)) return 100; + if (mode === 'hmac') { + if (tenant.domain && isSubdomainOf(host, tenant.domain)) return 75; + return 0; + } + for (const alias of readHostList(tenant.domains)) { if (eq(alias)) return 90; } @@ -102,16 +110,16 @@ export function scoreTenantHost( export function findTenantForHost( tenants: T[], host: string, + mode: TenantHostMatchMode = 'full', ): T | undefined { const scored = tenants - .map((row) => ({ row, score: scoreTenantHost(row, host) })) + .map((row) => ({ row, score: scoreTenantHost(row, host, mode) })) .filter((item) => item.score > 0) .sort((a, b) => b.score - a.score); if (!scored.length) return undefined; const best = scored[0].score; const tied = scored.filter((item) => item.score === best); - // nodeServer مشترک یا دو نسخه با امتیاز مساوی — حدس نزن if (tied.length !== 1) return undefined; return tied[0].row; }