import { config } from '../../core/config.js'; import { queryOne } from '../../core/db.js'; import { getHeader, type RequestContext } from '../../core/http.js'; interface TenantResolveRow { id: string; slug: string; name: string; status: string; mode: string; host: string | null; brand_name: string | null; short_name: string | null; slogan: string | null; logo_url: string | null; favicon_url: string | null; service_wechat: string | null; service_account_name: string | null; theme: Record; public_assets: Record; feature_flags: Record; admin_feature_flags: Record; public_config: Record; } function normalizeHost(host: string) { return host.split(':')[0]?.trim().toLowerCase() || ''; } export async function resolveTenantRoute(ctx: RequestContext) { const hostParam = ctx.url.searchParams.get('host') || ''; const tenantCode = ctx.url.searchParams.get('tenantCode') || getHeader(ctx.req, 'x-tenant-code'); const requestHost = normalizeHost(hostParam || getHeader(ctx.req, 'x-forwarded-host') || getHeader(ctx.req, 'host')); const row = tenantCode ? await queryOne( ` select t.id, t.slug, t.name, t.status, t.mode, null::text as host, b.brand_name, b.short_name, b.slogan, b.logo_url, b.favicon_url, b.service_wechat, b.service_account_name, coalesce(tc.active_theme, b.theme, '{}'::jsonb) as theme, coalesce(b.public_assets, tc.active_public_assets, '{}'::jsonb) as public_assets, coalesce(s.feature_flags, '{}'::jsonb) as feature_flags, coalesce(s.admin_feature_flags, '{}'::jsonb) as admin_feature_flags, coalesce(s.public_config, '{}'::jsonb) as public_config from public.tenants t left join public.tenant_branding b on b.tenant_id = t.id left join public.tenant_settings s on s.tenant_id = t.id left join public.tenant_theme_configs tc on tc.tenant_id = t.id and tc.status = 'published' where t.slug = $1 and t.status = 'active' limit 1 `, [tenantCode], ) : await queryOne( ` select t.id, t.slug, t.name, t.status, t.mode, d.host::text, b.brand_name, b.short_name, b.slogan, b.logo_url, b.favicon_url, b.service_wechat, b.service_account_name, coalesce(tc.active_theme, b.theme, '{}'::jsonb) as theme, coalesce(b.public_assets, tc.active_public_assets, '{}'::jsonb) as public_assets, coalesce(s.feature_flags, '{}'::jsonb) as feature_flags, coalesce(s.admin_feature_flags, '{}'::jsonb) as admin_feature_flags, coalesce(s.public_config, '{}'::jsonb) as public_config from public.tenant_domains d join public.tenants t on t.id = d.tenant_id left join public.tenant_branding b on b.tenant_id = t.id left join public.tenant_settings s on s.tenant_id = t.id left join public.tenant_theme_configs tc on tc.tenant_id = t.id and tc.status = 'published' where d.host = $1 and d.status = 'active' and t.status = 'active' limit 1 `, [requestHost || 'localhost'], ); if (!row && requestHost !== 'localhost') { ctx.url.searchParams.set('tenantCode', config.defaultTenantSlug); return resolveTenantRoute(ctx); } if (!row) { return { found: false, message: 'Tenant not found', lookup: { host: requestHost, tenantCode: tenantCode || null }, }; } return { found: true, tenant: { id: row.id, slug: row.slug, name: row.name, status: row.status, mode: row.mode, host: row.host, }, branding: { brandName: row.brand_name || row.name, shortName: row.short_name || row.name, slogan: row.slogan || '', logoUrl: row.logo_url || '', faviconUrl: row.favicon_url || '', serviceWechat: row.service_wechat || '', serviceAccountName: row.service_account_name || '', theme: row.theme || {}, publicAssets: row.public_assets || {}, }, features: row.feature_flags || {}, adminFeatures: row.admin_feature_flags || {}, publicConfig: row.public_config || {}, }; }