Files
api/src/auth/tenant-host.util.ts
T

118 lines
3.5 KiB
TypeScript
Raw Normal View History

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\./, '');
}
/** 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 URLs. nodeServer is last and only if unique (many versions
* share one API host like new.gholaktala.ir).
*/
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;
}
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)) {
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 {
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;
}