Fix SMS tenant copy target matching; add list-tenants helper.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-06 17:17:56 +03:30
co-authored by Cursor
parent 7ebee7fa4e
commit c6d90ea262
3 changed files with 145 additions and 27 deletions
+86 -26
View File
@@ -44,41 +44,71 @@ async function findTenantByDomain(domainRaw: string) {
const host = normalizeHost(domainRaw);
if (!host) throw new Error('domain is empty');
const byId = env('SMS_COPY_TO_TENANT_ID') || env('SMS_COPY_FROM_TENANT_ID');
// Prefer explicit ids when provided via dedicated envs in main().
const candidates = new Set<string>([host]);
if (host.startsWith('app.')) candidates.add(host.slice(4));
else candidates.add(`app.${host}`);
const tenants = await prisma.tenant.findMany({ where: { isActive: true } });
const hit = tenants.find((t) => {
const set = new Set<string>();
const primary = normalizeHost(t.domain);
if (primary) set.add(primary);
// 1) Exact primary domain
let hit = tenants.find((t) => candidates.has(normalizeHost(t.domain)));
if (hit) return hit;
// 2) Exact match in domains[] only (no nodeServer fuzzy)
hit = tenants.find((t) => {
for (const d of readDomains(t.domains)) {
const n = normalizeHost(d);
if (n) set.add(n);
}
for (const c of candidates) {
if (set.has(c)) return true;
}
const node = normalizeHost(t.nodeServer);
for (const c of candidates) {
if (node.includes(c)) return true;
}
const appUrl = normalizeHost(t.appUrl);
for (const c of candidates) {
if (appUrl === c) return true;
if (candidates.has(normalizeHost(d))) return true;
}
return false;
});
if (hit) return hit;
if (!hit) {
const sample = tenants
.filter((t) => /ayg|montzar|آیگ|ایگ/i.test(`${t.domain} ${t.nameFa} ${t.nodeServer}`))
.map((t) => ({ id: t.id, domain: t.domain, nameFa: t.nameFa, nodeServer: t.nodeServer }));
console.error('Nearby tenants:', sample);
throw new Error(`No active tenant matched domain: ${host}`);
// 3) Exact appUrl host
hit = tenants.find((t) => candidates.has(normalizeHost(t.appUrl)));
if (hit) return hit;
const sample = tenants
.filter((t) => /ayg|montzar|آیگ|ایگ/i.test(`${t.domain} ${t.nameFa} ${t.nodeServer} ${t.appUrl}`))
.map((t) => ({
id: t.id,
domain: t.domain,
nameFa: t.nameFa,
nodeServer: t.nodeServer,
appUrl: t.appUrl,
domains: t.domains,
}));
console.error('Nearby tenants:', sample);
throw new Error(`No active tenant matched domain exactly: ${host}`);
}
async function findTenant(opts: {
domain?: string;
tenantId?: string;
nameFa?: string;
}) {
if (opts.tenantId) {
const t = await prisma.tenant.findUnique({ where: { id: opts.tenantId } });
if (!t) throw new Error(`Tenant id not found: ${opts.tenantId}`);
return t;
}
return hit;
if (opts.nameFa) {
const name = opts.nameFa.trim();
const rows = await prisma.tenant.findMany({
where: { isActive: true, nameFa: { contains: name } },
});
if (rows.length === 1) return rows[0];
if (rows.length === 0) throw new Error(`No tenant nameFa contains: ${name}`);
console.error(
'Multiple name matches:',
rows.map((t) => ({ id: t.id, nameFa: t.nameFa, domain: t.domain })),
);
throw new Error(`Ambiguous nameFa: ${name} (use SMS_COPY_TO_TENANT_ID)`);
}
if (!opts.domain) throw new Error('domain/tenantId/nameFa required');
return findTenantByDomain(opts.domain);
}
async function main() {
@@ -88,8 +118,38 @@ async function main() {
const withHistory = env('SMS_COPY_HISTORY', '1') !== '0';
const addDomainAlias = env('SMS_COPY_ADD_ALIAS', '1') !== '0';
const fromTenant = await findTenantByDomain(fromDomain);
const toTenant = await findTenantByDomain(toDomain);
// If a previous run wrongly attached app.aygingold.ir onto k.montzar.ir, strip it first
// unless source and target are intentionally the same.
const stripWrongAlias = env('SMS_COPY_STRIP_ALIAS_FROM', 'k.montzar.ir');
if (stripWrongAlias && normalizeHost(stripWrongAlias) !== normalizeHost(toDomain)) {
const maybe = await prisma.tenant.findFirst({
where: { domain: normalizeHost(stripWrongAlias) },
});
if (maybe) {
const hostTo = normalizeHost(toDomain);
const current = readDomains(maybe.domains).map(normalizeHost).filter(Boolean);
if (current.includes(hostTo)) {
const next = current.filter((d) => d !== hostTo);
console.log(`Stripping alias ${hostTo} from ${maybe.domain} domains:`, current, '=>', next);
if (!dryRun) {
await prisma.tenant.update({
where: { id: maybe.id },
data: { domains: next },
});
}
}
}
}
const fromTenant = await findTenant({
tenantId: env('SMS_COPY_FROM_TENANT_ID') || undefined,
domain: fromDomain,
});
const toTenant = await findTenant({
tenantId: env('SMS_COPY_TO_TENANT_ID') || undefined,
nameFa: env('SMS_COPY_TO_NAME_FA') || undefined,
domain: env('SMS_COPY_TO_TENANT_ID') || env('SMS_COPY_TO_NAME_FA') ? undefined : toDomain,
});
const fromAccount = await prisma.smsAccount.findUnique({
where: { tenantId: fromTenant.id },