Files
gongxue-base/apps/api/src/features/tenant/routes.ts
2026-06-29 18:05:30 +08:00

117 lines
4.3 KiB
TypeScript

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<string, unknown>;
public_assets: Record<string, unknown>;
feature_flags: Record<string, unknown>;
admin_feature_flags: Record<string, unknown>;
public_config: Record<string, unknown>;
}
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<TenantResolveRow>(
`
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<TenantResolveRow>(
`
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 || {},
};
}