Prefer exact tenant domain over shared nodeServer for remote HMAC auth.
Stops demo and shop rows that share one API URL from colliding, and skips phone-like adminUrl values when resolving X-Abshode-Domain. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,11 +10,21 @@ import { PrismaService } from '../prisma/prisma.service';
|
|||||||
|
|
||||||
const MAX_SKEW_SECONDS = 300;
|
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
|
* 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}
|
* 2) Fallback: Authorization Bearer {Tenant.remoteApiToken}
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -128,6 +138,9 @@ export class RemoteSmsAuthGuard implements CanActivate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async findTenantByDomain(host: string) {
|
private async findTenantByDomain(host: string) {
|
||||||
|
const normalizedHost = this.normalizeHost(host);
|
||||||
|
if (!normalizedHost) return null;
|
||||||
|
|
||||||
const tenants = await this.prisma.tenant.findMany({
|
const tenants = await this.prisma.tenant.findMany({
|
||||||
where: { isActive: true },
|
where: { isActive: true },
|
||||||
select: {
|
select: {
|
||||||
@@ -143,43 +156,63 @@ export class RemoteSmsAuthGuard implements CanActivate {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const hit = tenants.find((row) => this.matchesHost(row, host));
|
let best: { row: TenantHostRow; score: number } | null = null;
|
||||||
if (!hit) return 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 {
|
return {
|
||||||
id: hit.id,
|
id: best.row.id,
|
||||||
nameFa: hit.nameFa,
|
nameFa: best.row.nameFa,
|
||||||
domain: hit.domain,
|
domain: best.row.domain,
|
||||||
product: hit.product,
|
product: best.row.product,
|
||||||
isActive: hit.isActive,
|
isActive: best.row.isActive,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private matchesHost(
|
/**
|
||||||
tenant: {
|
* امتیاز بالاتر = هویت دقیقتر نسخه.
|
||||||
domain: string | null;
|
* nodeServer ضعیف است چون چند نسخه ممکن است یک API مشترک داشته باشند.
|
||||||
domains: unknown;
|
*/
|
||||||
adminUrl?: string | null;
|
private scoreHostMatch(tenant: TenantHostRow, host: string): number {
|
||||||
appUrl?: string | null;
|
|
||||||
nodeServer?: string | null;
|
|
||||||
},
|
|
||||||
host: string,
|
|
||||||
) {
|
|
||||||
const hostNoWww = host.replace(/^www\./, '');
|
const hostNoWww = host.replace(/^www\./, '');
|
||||||
const values = [
|
const eq = (raw?: string | null) => {
|
||||||
tenant.domain,
|
const normalized = this.normalizeHost(raw);
|
||||||
tenant.adminUrl,
|
if (!normalized) return false;
|
||||||
tenant.appUrl,
|
return (
|
||||||
tenant.nodeServer,
|
normalized === host || normalized.replace(/^www\./, '') === hostNoWww
|
||||||
...this.readDomains(tenant.domains),
|
);
|
||||||
];
|
};
|
||||||
for (const value of values) {
|
|
||||||
const normalized = this.normalizeHost(value);
|
if (eq(tenant.domain)) return 100;
|
||||||
if (!normalized) continue;
|
|
||||||
if (normalized === host || normalized.replace(/^www\./, '') === hostNoWww) {
|
for (const alias of this.readDomains(tenant.domains)) {
|
||||||
return true;
|
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[] {
|
private readDomains(raw: unknown): string[] {
|
||||||
|
|||||||
Reference in New Issue
Block a user