diff --git a/src/auth/remote-sms-auth.guard.ts b/src/auth/remote-sms-auth.guard.ts index 900ab74..b2e6f28 100644 --- a/src/auth/remote-sms-auth.guard.ts +++ b/src/auth/remote-sms-auth.guard.ts @@ -10,11 +10,21 @@ import { PrismaService } from '../prisma/prisma.service'; const MAX_SKEW_SECONDS = 300; +type TenantHostRow = { + id: string; + nameFa: string; + domain: string | null; + domains: unknown; + adminUrl?: string | null; + appUrl?: string | null; + nodeServer?: string | null; + product: string; + isActive: boolean; +}; + /** - * Auth for product → SuperAdmin SMS: + * Auth for product → SuperAdmin SMS / plugin-store: * 1) Preferred: shared HMAC + domain headers - * X-Abshode-Domain, X-Abshode-Timestamp, X-Abshode-Signature - * payload = `${timestamp}.${METHOD}.${canonicalPath}.${domain}` * 2) Fallback: Authorization Bearer {Tenant.remoteApiToken} */ @Injectable() @@ -128,6 +138,9 @@ export class RemoteSmsAuthGuard implements CanActivate { } private async findTenantByDomain(host: string) { + const normalizedHost = this.normalizeHost(host); + if (!normalizedHost) return null; + const tenants = await this.prisma.tenant.findMany({ where: { isActive: true }, select: { @@ -143,43 +156,63 @@ export class RemoteSmsAuthGuard implements CanActivate { }, }); - const hit = tenants.find((row) => this.matchesHost(row, host)); - if (!hit) return null; + let best: { row: TenantHostRow; score: number } | null = null; + for (const row of tenants) { + const score = this.scoreHostMatch(row, normalizedHost); + if (score <= 0) continue; + if (!best || score > best.score) { + best = { row, score }; + } + } + if (!best) return null; + return { - id: hit.id, - nameFa: hit.nameFa, - domain: hit.domain, - product: hit.product, - isActive: hit.isActive, + id: best.row.id, + nameFa: best.row.nameFa, + domain: best.row.domain, + product: best.row.product, + isActive: best.row.isActive, }; } - private matchesHost( - tenant: { - domain: string | null; - domains: unknown; - adminUrl?: string | null; - appUrl?: string | null; - nodeServer?: string | null; - }, - host: string, - ) { + /** + * امتیاز بالاتر = هویت دقیق‌تر نسخه. + * nodeServer ضعیف است چون چند نسخه ممکن است یک API مشترک داشته باشند. + */ + private scoreHostMatch(tenant: TenantHostRow, host: string): number { const hostNoWww = host.replace(/^www\./, ''); - const values = [ - tenant.domain, - tenant.adminUrl, - tenant.appUrl, - tenant.nodeServer, - ...this.readDomains(tenant.domains), - ]; - for (const value of values) { - const normalized = this.normalizeHost(value); - if (!normalized) continue; - if (normalized === host || normalized.replace(/^www\./, '') === hostNoWww) { - return true; - } + const eq = (raw?: string | null) => { + const normalized = this.normalizeHost(raw); + if (!normalized) return false; + return ( + normalized === host || normalized.replace(/^www\./, '') === hostNoWww + ); + }; + + if (eq(tenant.domain)) return 100; + + for (const alias of this.readDomains(tenant.domains)) { + if (eq(alias)) return 90; } - return false; + + if (eq(tenant.appUrl)) return 80; + + // adminUrl گاهی موبایل است؛ فقط اگر شبیه میزبان باشد + if (this.looksLikeHostname(tenant.adminUrl) && eq(tenant.adminUrl)) { + return 50; + } + + if (eq(tenant.nodeServer)) return 10; + + return 0; + } + + private looksLikeHostname(value?: string | null) { + const normalized = this.normalizeHost(value); + if (!normalized || !normalized.includes('.')) return false; + if (/^\d+(\.\d+)*$/.test(normalized)) return false; + if (/^09\d{9}$/.test(normalized)) return false; + return true; } private readDomains(raw: unknown): string[] {