forked from wangziqi/gongxue-base
193 lines
6.3 KiB
TypeScript
193 lines
6.3 KiB
TypeScript
import { query, queryOne } from '../../core/db.js';
|
|
import { HttpError } from '../../core/http.js';
|
|
import { config as appConfig } from '../../core/config.js';
|
|
|
|
export type TenantSecretScope = 'payment' | 'sms' | 'oauth' | 'storage' | 'crm' | 'ai' | 'system';
|
|
|
|
export interface TenantSecretConfig {
|
|
secretScope: TenantSecretScope;
|
|
secretKey: string;
|
|
secretValue: string | null;
|
|
secretJson: Record<string, unknown>;
|
|
}
|
|
|
|
export interface TenantAuthProviderConfig {
|
|
tenantId: string;
|
|
provider: string;
|
|
status: 'active' | 'testing' | 'disabled';
|
|
configPublic: Record<string, unknown>;
|
|
secret: TenantSecretConfig | null;
|
|
}
|
|
|
|
interface TenantAuthProviderRow {
|
|
provider: string;
|
|
status: 'active' | 'testing' | 'disabled';
|
|
configPublic: Record<string, unknown> | null;
|
|
}
|
|
|
|
interface TenantSecretRow {
|
|
secretScope: TenantSecretScope;
|
|
secretKey: string;
|
|
secretValue: string | null;
|
|
secretJson: Record<string, unknown> | null;
|
|
}
|
|
|
|
function objectValue(value: unknown): Record<string, unknown> {
|
|
return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
|
}
|
|
|
|
function parseSecretRef(value: unknown): { scope: TenantSecretScope; key: string } | null {
|
|
if (typeof value !== 'string' || !value.trim()) return null;
|
|
const parts = value.trim().split(':');
|
|
if (parts.length !== 3 || parts[0] !== 'app_private.tenant_secrets') return null;
|
|
const scope = parts[1] as TenantSecretScope;
|
|
const key = parts[2];
|
|
if (!['payment', 'sms', 'oauth', 'storage', 'crm', 'ai', 'system'].includes(scope) || !key) return null;
|
|
return { scope, key };
|
|
}
|
|
|
|
function defaultSecretScope(provider: string): TenantSecretScope {
|
|
const normalized = provider.toLowerCase().replace(/[_\s]/g, '-');
|
|
if (normalized.includes('sms') || normalized === 'aliyun' || normalized === 'tencent') return 'sms';
|
|
return 'oauth';
|
|
}
|
|
|
|
async function loadTenantSecret(
|
|
tenantId: string,
|
|
configPublic: Record<string, unknown>,
|
|
provider: string,
|
|
): Promise<TenantSecretConfig | null> {
|
|
const secretRef = parseSecretRef(configPublic.secretRef);
|
|
const scope = secretRef?.scope || defaultSecretScope(provider);
|
|
const key = secretRef?.key || provider;
|
|
|
|
const row = await queryOne<TenantSecretRow>(
|
|
`
|
|
select secret_scope as "secretScope", secret_key as "secretKey",
|
|
secret_value as "secretValue", secret_json as "secretJson"
|
|
from app_private.tenant_secrets
|
|
where tenant_id = $1
|
|
and secret_scope = $2
|
|
and secret_key = $3
|
|
limit 1
|
|
`,
|
|
[tenantId, scope, key],
|
|
);
|
|
|
|
if (!row) return null;
|
|
return {
|
|
secretScope: row.secretScope,
|
|
secretKey: row.secretKey,
|
|
secretValue: row.secretValue,
|
|
secretJson: objectValue(row.secretJson),
|
|
};
|
|
}
|
|
|
|
export async function loadTenantAuthProviderConfig(
|
|
tenantId: string,
|
|
providers: string[],
|
|
): Promise<TenantAuthProviderConfig | null> {
|
|
const uniqueProviders = [...new Set(providers.map(item => item.trim()).filter(Boolean))];
|
|
if (uniqueProviders.length === 0) return null;
|
|
|
|
const rows = await query<TenantAuthProviderRow>(
|
|
`
|
|
select provider, status, config_public as "configPublic"
|
|
from public.tenant_auth_providers
|
|
where tenant_id = $1
|
|
and provider = any($2::text[])
|
|
and status in ('active', 'testing')
|
|
`,
|
|
[tenantId, uniqueProviders],
|
|
);
|
|
|
|
const row = uniqueProviders.map(provider => rows.find(item => item.provider === provider)).find(Boolean);
|
|
if (!row) return null;
|
|
|
|
const configPublic = objectValue(row.configPublic);
|
|
const secret = await loadTenantSecret(tenantId, configPublic, row.provider);
|
|
return {
|
|
tenantId,
|
|
provider: row.provider,
|
|
status: row.status,
|
|
configPublic,
|
|
secret,
|
|
};
|
|
}
|
|
|
|
export function requirePublicString(config: TenantAuthProviderConfig, keys: string[], code: string) {
|
|
for (const key of keys) {
|
|
const value = config.configPublic[key];
|
|
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
}
|
|
throw new HttpError(503, `${config.provider} public config is missing ${keys[0]}`, code);
|
|
}
|
|
|
|
export function optionalPublicString(config: TenantAuthProviderConfig, keys: string[]) {
|
|
for (const key of keys) {
|
|
const value = config.configPublic[key];
|
|
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function isLocalDevHost(hostname: string) {
|
|
return ['127.0.0.1', 'localhost', '::1'].includes(hostname);
|
|
}
|
|
|
|
function isAllowedHost(hostname: string, allowedHosts: string[]) {
|
|
return allowedHosts.some(host => hostname === host || hostname.endsWith(`.${host}`));
|
|
}
|
|
|
|
export function providerEndpoint(
|
|
authProvider: TenantAuthProviderConfig,
|
|
fallback: string,
|
|
allowedHosts: string[],
|
|
code = 'PROVIDER_ENDPOINT_NOT_ALLOWED',
|
|
) {
|
|
const raw = optionalPublicString(authProvider, ['endpoint']) || fallback;
|
|
let endpoint: URL;
|
|
try {
|
|
endpoint = new URL(raw);
|
|
} catch {
|
|
throw new HttpError(503, `${authProvider.provider} endpoint is invalid`, code);
|
|
}
|
|
|
|
const localDev = !appConfig.isProduction && isLocalDevHost(endpoint.hostname);
|
|
if (!localDev && endpoint.protocol !== 'https:') {
|
|
throw new HttpError(503, `${authProvider.provider} endpoint must use HTTPS`, code);
|
|
}
|
|
if (!localDev && !isAllowedHost(endpoint.hostname, allowedHosts)) {
|
|
throw new HttpError(503, `${authProvider.provider} endpoint host is not allowed`, code);
|
|
}
|
|
endpoint.username = '';
|
|
endpoint.password = '';
|
|
return endpoint.toString();
|
|
}
|
|
|
|
export function optionalPublicObject(config: TenantAuthProviderConfig, key: string) {
|
|
return objectValue(config.configPublic[key]);
|
|
}
|
|
|
|
export function optionalPublicArray(config: TenantAuthProviderConfig, key: string) {
|
|
const value = config.configPublic[key];
|
|
return Array.isArray(value) ? value : [];
|
|
}
|
|
|
|
export function secretString(config: TenantAuthProviderConfig, keys: string[]) {
|
|
const secret = config.secret;
|
|
if (!secret) return '';
|
|
if (typeof secret.secretValue === 'string' && secret.secretValue.trim()) return secret.secretValue.trim();
|
|
for (const key of keys) {
|
|
const value = secret.secretJson[key];
|
|
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
}
|
|
return '';
|
|
}
|
|
|
|
export function requireSecretString(config: TenantAuthProviderConfig, keys: string[], code: string) {
|
|
const value = secretString(config, keys);
|
|
if (value) return value;
|
|
throw new HttpError(503, `${config.provider} secret is missing ${keys[0]}`, code);
|
|
}
|