diff --git a/src/auth/tenant-host.util.spec.ts b/src/auth/tenant-host.util.spec.ts new file mode 100644 index 0000000..650a57b --- /dev/null +++ b/src/auth/tenant-host.util.spec.ts @@ -0,0 +1,51 @@ +import { findTenantForHost, scoreTenantHost } from './tenant-host.util'; + +const gholak = { + domain: 'gholaktala.ir', + domains: [], + appUrl: 'https://gholaktala.ir', + adminUrl: 'https://admin.gholaktala.ir', + nodeServer: 'https://new.gholaktala.ir/server', +}; + +const montzar = { + domain: 'k.montzar.ir', + domains: ['montzar.ir'], + appUrl: 'https://k.montzar.ir', + adminUrl: 'https://k.montzar.ir', + nodeServer: 'https://new.gholaktala.ir/server', +}; + +const otherOnSameBox = { + domain: 'examplegold.ir', + domains: [], + appUrl: 'https://examplegold.ir', + adminUrl: null, + nodeServer: 'https://new.gholaktala.ir/server', +}; + +describe('findTenantForHost', () => { + const tenants = [montzar, otherOnSameBox, gholak]; + + it('maps new.gholaktala.ir to gholaktala, not the first shared nodeServer tenant', () => { + expect(findTenantForHost(tenants, 'new.gholaktala.ir')?.domain).toBe( + 'gholaktala.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', () => { + expect(findTenantForHost(tenants, 'k.montzar.ir')?.domain).toBe( + 'k.montzar.ir', + ); + }); + + it('does not pick a random tenant when only nodeServer matches several rows', () => { + const sharedOnly = [ + { ...montzar, domain: 'a.ir', domains: [] }, + { ...gholak, domain: 'b.ir', domains: [] }, + ]; + expect(findTenantForHost(sharedOnly, 'new.gholaktala.ir')).toBeUndefined(); + }); +}); diff --git a/src/auth/tenant-host.util.ts b/src/auth/tenant-host.util.ts index 13369bf..18195cc 100644 --- a/src/auth/tenant-host.util.ts +++ b/src/auth/tenant-host.util.ts @@ -54,10 +54,18 @@ function hostsEqual(left: string, right: string): boolean { return left.replace(/^www\./, '') === right.replace(/^www\./, ''); } +/** new.gholaktala.ir belongs to tenant domain gholaktala.ir — not via shared nodeServer. */ +function isSubdomainOf(host: string, parent: string): boolean { + const child = normalizeTenantHost(host); + const root = normalizeTenantHost(parent).replace(/^www\./, ''); + if (!child || !root || !root.includes('.')) return false; + return child === root || child.endsWith('.' + root); +} + /** * Same identity as GET /get_user?domain= — primary domain, then aliases, - * then app/admin/node URLs so HMAC and get_user cannot disagree. - * Origin پنل ادمین (دامنه یا آی‌پی LAN داخل adminUrl) هم همین‌جا مچ می‌شود. + * then app/admin URLs. nodeServer is last and only if unique (many versions + * share one API host like new.gholaktala.ir). */ export function scoreTenantHost( tenant: TenantHostFields, @@ -75,6 +83,10 @@ export function scoreTenantHost( for (const value of readHostList(tenant.appUrl)) { if (eq(value)) return 80; } + if (tenant.domain && isSubdomainOf(host, tenant.domain)) return 75; + for (const alias of readHostList(tenant.domains)) { + if (isSubdomainOf(host, alias)) return 72; + } for (const value of readHostList(tenant.adminUrl)) { const normalized = normalizeTenantHost(value); if (looksLikeHostname(normalized) && hostsEqual(normalized, host)) { @@ -91,11 +103,15 @@ export function findTenantForHost( tenants: T[], host: string, ): T | undefined { - let best: { row: T; score: number } | undefined; - for (const row of tenants) { - const score = scoreTenantHost(row, host); - if (score <= 0) continue; - if (!best || score > best.score) best = { row, score }; - } - return best?.row; + const scored = tenants + .map((row) => ({ row, score: scoreTenantHost(row, host) })) + .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; } diff --git a/src/plugins/plugin-store.service.ts b/src/plugins/plugin-store.service.ts index d10c193..22d5765 100644 --- a/src/plugins/plugin-store.service.ts +++ b/src/plugins/plugin-store.service.ts @@ -810,7 +810,17 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy { } if (!tenantId) throw new ForbiddenException('نسخه نامعتبر است'); if (!(await this.isStoreEnabled(tenantId))) { - throw new ForbiddenException('فروشگاه افزونه برای این نسخه خاموش است'); + const tenant = await this.prisma.tenant.findUnique({ + where: { id: tenantId }, + select: { nameFa: true, domain: true }, + }); + const who = tenant + ? `${tenant.nameFa} (${tenant.domain})` + : tenantId; + this.logger.warn(`plugin store off for ${who}`); + throw new ForbiddenException( + `فروشگاه افزونه برای این نسخه خاموش است: ${who}`, + ); } }