2026-09-08 10:50:40 +03:30
|
|
|
export type TenantHostFields = {
|
|
|
|
|
domain?: string | null;
|
|
|
|
|
domains?: unknown;
|
|
|
|
|
adminUrl?: string | null;
|
|
|
|
|
appUrl?: string | null;
|
|
|
|
|
nodeServer?: string | null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function normalizeTenantHost(value?: string | null): string {
|
|
|
|
|
if (!value) return '';
|
|
|
|
|
return String(value)
|
|
|
|
|
.trim()
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
.replace(/^https?:\/\//, '')
|
|
|
|
|
.split('/')[0]
|
|
|
|
|
.split(':')[0]
|
|
|
|
|
.replace(/\.$/, '');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** aliases JSON array, object map, CSV, or a single host/URL */
|
|
|
|
|
export function readHostList(raw: unknown): string[] {
|
|
|
|
|
if (!raw) return [];
|
|
|
|
|
if (Array.isArray(raw)) return raw.flatMap((item) => readHostList(item));
|
|
|
|
|
if (typeof raw === 'object') {
|
|
|
|
|
return Object.values(raw as Record<string, unknown>).flatMap(readHostList);
|
|
|
|
|
}
|
|
|
|
|
const text = String(raw).trim();
|
|
|
|
|
if (!text) return [];
|
|
|
|
|
if (
|
|
|
|
|
(text.startsWith('[') && text.endsWith(']')) ||
|
|
|
|
|
(text.startsWith('{') && text.endsWith('}'))
|
|
|
|
|
) {
|
|
|
|
|
try {
|
|
|
|
|
return readHostList(JSON.parse(text));
|
|
|
|
|
} catch {
|
|
|
|
|
/* fall through */
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return text
|
|
|
|
|
.split(/[\s,;]+/)
|
|
|
|
|
.map((item) => item.trim())
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function looksLikeHostname(value: string): boolean {
|
|
|
|
|
if (!value.includes('.')) return false;
|
|
|
|
|
if (/^09\d{9}$/.test(value)) return false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hostsEqual(left: string, right: string): boolean {
|
|
|
|
|
if (!left || !right) return false;
|
|
|
|
|
if (left === right) return true;
|
|
|
|
|
return left.replace(/^www\./, '') === right.replace(/^www\./, '');
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-09 21:53:11 +03:30
|
|
|
/** 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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-08 10:50:40 +03:30
|
|
|
/**
|
|
|
|
|
* Same identity as GET /get_user?domain= — primary domain, then aliases,
|
2026-09-09 21:53:11 +03:30
|
|
|
* then app/admin URLs. nodeServer is last and only if unique (many versions
|
|
|
|
|
* share one API host like new.gholaktala.ir).
|
2026-09-08 10:50:40 +03:30
|
|
|
*/
|
|
|
|
|
export function scoreTenantHost(
|
|
|
|
|
tenant: TenantHostFields,
|
|
|
|
|
hostRaw: string,
|
|
|
|
|
): number {
|
|
|
|
|
const host = normalizeTenantHost(hostRaw);
|
|
|
|
|
if (!host) return 0;
|
|
|
|
|
const eq = (raw?: string | null) =>
|
|
|
|
|
hostsEqual(normalizeTenantHost(raw), host);
|
|
|
|
|
|
|
|
|
|
if (eq(tenant.domain)) return 100;
|
|
|
|
|
for (const alias of readHostList(tenant.domains)) {
|
|
|
|
|
if (eq(alias)) return 90;
|
|
|
|
|
}
|
|
|
|
|
for (const value of readHostList(tenant.appUrl)) {
|
|
|
|
|
if (eq(value)) return 80;
|
|
|
|
|
}
|
2026-09-09 21:53:11 +03:30
|
|
|
if (tenant.domain && isSubdomainOf(host, tenant.domain)) return 75;
|
|
|
|
|
for (const alias of readHostList(tenant.domains)) {
|
|
|
|
|
if (isSubdomainOf(host, alias)) return 72;
|
|
|
|
|
}
|
2026-09-08 10:50:40 +03:30
|
|
|
for (const value of readHostList(tenant.adminUrl)) {
|
|
|
|
|
const normalized = normalizeTenantHost(value);
|
|
|
|
|
if (looksLikeHostname(normalized) && hostsEqual(normalized, host)) {
|
|
|
|
|
return 50;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (const value of readHostList(tenant.nodeServer)) {
|
|
|
|
|
if (eq(value)) return 10;
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function findTenantForHost<T extends TenantHostFields>(
|
|
|
|
|
tenants: T[],
|
|
|
|
|
host: string,
|
|
|
|
|
): T | undefined {
|
2026-09-09 21:53:11 +03:30
|
|
|
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;
|
2026-09-08 10:50:40 +03:30
|
|
|
}
|