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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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;
|
if (!hit) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { findTenantForHost, scoreTenantHost } from './tenant-host.util';
|
import { findTenantForHost, scoreTenantHost } from './tenant-host.util';
|
||||||
|
|
||||||
const gholak = {
|
const gholak = {
|
||||||
domain: 'gholaktala.ir',
|
domain: 'app.gholaktala.ir',
|
||||||
domains: [],
|
domains: [],
|
||||||
appUrl: 'https://gholaktala.ir',
|
appUrl: 'https://app.gholaktala.ir',
|
||||||
adminUrl: 'https://admin.gholaktala.ir',
|
adminUrl: 'https://k.montzar.ir/admin',
|
||||||
nodeServer: 'https://new.gholaktala.ir/server',
|
nodeServer: 'https://new.gholaktala.ir/server',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -25,22 +25,36 @@ const otherOnSameBox = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('findTenantForHost', () => {
|
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', () => {
|
it('hmac does not treat shared adminUrl as gholaktala', () => {
|
||||||
expect(findTenantForHost(tenants, 'new.gholaktala.ir')?.domain).toBe(
|
expect(scoreTenantHost(gholak, 'k.montzar.ir', 'hmac')).toBe(0);
|
||||||
'gholaktala.ir',
|
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(
|
expect(findTenantForHost(tenants, 'k.montzar.ir')?.domain).toBe(
|
||||||
'k.montzar.ir',
|
'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', () => {
|
it('does not pick a random tenant when only nodeServer matches several rows', () => {
|
||||||
const sharedOnly = [
|
const sharedOnly = [
|
||||||
{ ...montzar, domain: 'a.ir', domains: [] },
|
{ ...montzar, domain: 'a.ir', domains: [] },
|
||||||
|
|||||||
@@ -62,14 +62,17 @@ function isSubdomainOf(host: string, parent: string): boolean {
|
|||||||
return child === root || child.endsWith('.' + root);
|
return child === root || child.endsWith('.' + root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TenantHostMatchMode = 'full' | 'hmac';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Same identity as GET /get_user?domain= — primary domain, then aliases,
|
* full: get_user — دامنه، alias، app/admin/node.
|
||||||
* then app/admin URLs. nodeServer is last and only if unique (many versions
|
* hmac: فقط دامنهٔ اصلی نسخه (و سابدامنهٔ همان). adminUrl مشترک
|
||||||
* share one API host like new.gholaktala.ir).
|
* (k.montzar.ir روی نسخهٔ app.gholaktala.ir) هویت نیست.
|
||||||
*/
|
*/
|
||||||
export function scoreTenantHost(
|
export function scoreTenantHost(
|
||||||
tenant: TenantHostFields,
|
tenant: TenantHostFields,
|
||||||
hostRaw: string,
|
hostRaw: string,
|
||||||
|
mode: TenantHostMatchMode = 'full',
|
||||||
): number {
|
): number {
|
||||||
const host = normalizeTenantHost(hostRaw);
|
const host = normalizeTenantHost(hostRaw);
|
||||||
if (!host) return 0;
|
if (!host) return 0;
|
||||||
@@ -77,6 +80,11 @@ export function scoreTenantHost(
|
|||||||
hostsEqual(normalizeTenantHost(raw), host);
|
hostsEqual(normalizeTenantHost(raw), host);
|
||||||
|
|
||||||
if (eq(tenant.domain)) return 100;
|
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)) {
|
for (const alias of readHostList(tenant.domains)) {
|
||||||
if (eq(alias)) return 90;
|
if (eq(alias)) return 90;
|
||||||
}
|
}
|
||||||
@@ -102,16 +110,16 @@ export function scoreTenantHost(
|
|||||||
export function findTenantForHost<T extends TenantHostFields>(
|
export function findTenantForHost<T extends TenantHostFields>(
|
||||||
tenants: T[],
|
tenants: T[],
|
||||||
host: string,
|
host: string,
|
||||||
|
mode: TenantHostMatchMode = 'full',
|
||||||
): T | undefined {
|
): T | undefined {
|
||||||
const scored = tenants
|
const scored = tenants
|
||||||
.map((row) => ({ row, score: scoreTenantHost(row, host) }))
|
.map((row) => ({ row, score: scoreTenantHost(row, host, mode) }))
|
||||||
.filter((item) => item.score > 0)
|
.filter((item) => item.score > 0)
|
||||||
.sort((a, b) => b.score - a.score);
|
.sort((a, b) => b.score - a.score);
|
||||||
if (!scored.length) return undefined;
|
if (!scored.length) return undefined;
|
||||||
|
|
||||||
const best = scored[0].score;
|
const best = scored[0].score;
|
||||||
const tied = scored.filter((item) => item.score === best);
|
const tied = scored.filter((item) => item.score === best);
|
||||||
// nodeServer مشترک یا دو نسخه با امتیاز مساوی — حدس نزن
|
|
||||||
if (tied.length !== 1) return undefined;
|
if (tied.length !== 1) return undefined;
|
||||||
return tied[0].row;
|
return tied[0].row;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user