Files
Jyotisha/frontend/src/lib/epay/config-core.ts
T

110 lines
3.5 KiB
TypeScript

import { assertConfiguredEpayUrl } from "./gateway-policy";
import { decryptEpayKey } from "./encryption-core";
export type EpaySettingsRow = {
gateway_url: string;
pid: string;
encrypted_key: string;
notify_url: string;
return_url: string;
site_name: string;
chat_enabled: boolean;
};
export class EpayConfigurationError extends Error {
constructor(message = "易支付配置不可用") {
super(message);
this.name = "EpayConfigurationError";
}
}
function validEpayUrl(value: string, label: string, env: NodeJS.ProcessEnv) {
try {
return assertConfiguredEpayUrl(value, env);
} catch {
throw new EpayConfigurationError(`${label} 无效`);
}
}
export function suggestedEpayUrls(
siteAddress = process.env.SITE_ADDRESS,
env: NodeJS.ProcessEnv = process.env,
) {
let base: URL;
try {
base = validEpayUrl(siteAddress?.trim() || "", "站点地址", env);
} catch {
const allowLoopbackHttp = (env.NODE_ENV === "development" || env.NODE_ENV === "test")
&& ["true", "1"].includes(env.EPAY_ALLOW_INSECURE_LOOPBACK_HTTP?.trim().toLowerCase() || "");
base = new URL(allowLoopbackHttp ? "http://localhost:3000" : "https://localhost:3000");
}
return {
notifyUrl: new URL("/api/payment/epay/notify", base).toString(),
returnUrl: new URL("/", base).toString(),
};
}
function environmentChatEnabled(value: string | undefined) {
const normalized = value?.trim().toLowerCase();
return normalized === "true" || normalized === "1";
}
function completeConfig(values: {
gateway: string;
pid: string;
key: string;
notifyUrl: string;
returnUrl: string;
siteName: string;
chatEnabled: boolean;
}, env: NodeJS.ProcessEnv) {
if (!values.gateway || !values.pid || !values.key || !values.notifyUrl || !values.returnUrl || !values.siteName) {
throw new EpayConfigurationError();
}
const gatewayUrl = validEpayUrl(values.gateway.replace(/\/+$/, ""), "易支付网关地址", env);
validEpayUrl(values.notifyUrl, "异步通知地址", env);
validEpayUrl(values.returnUrl, "支付返回地址", env);
return { gatewayUrl, pid: values.pid, key: values.key, notifyUrl: values.notifyUrl, returnUrl: values.returnUrl, siteName: values.siteName, chatEnabled: values.chatEnabled };
}
export async function resolveEpayConfig(
loadDatabaseRow: () => Promise<EpaySettingsRow | null>,
env: NodeJS.ProcessEnv = process.env,
) {
let row: EpaySettingsRow | null;
try {
row = await loadDatabaseRow();
} catch {
throw new EpayConfigurationError();
}
if (row) {
return completeConfig({
gateway: row.gateway_url.trim(),
pid: row.pid.trim(),
key: decryptEpayKey(row.encrypted_key, env.EPAY_CONFIG_ENCRYPTION_KEY),
notifyUrl: row.notify_url.trim(),
returnUrl: row.return_url.trim(),
siteName: row.site_name.trim(),
chatEnabled: row.chat_enabled,
}, env);
}
const defaults = suggestedEpayUrls(env.SITE_ADDRESS, env);
return completeConfig({
gateway: env.EPAY_GATEWAY_URL?.trim() || "",
pid: env.EPAY_PID?.trim() || "",
key: env.EPAY_KEY?.trim() || "",
notifyUrl: env.EPAY_NOTIFY_URL?.trim() || defaults.notifyUrl,
returnUrl: env.EPAY_RETURN_URL?.trim() || defaults.returnUrl,
siteName: env.EPAY_SITE_NAME?.trim() || "Jyotisha",
chatEnabled: environmentChatEnabled(env.EPAY_CHAT_ENABLED),
}, env);
}
export function epaySubmitUrl(gatewayUrl: URL) {
const url = new URL(gatewayUrl.toString());
url.pathname = `${url.pathname.replace(/\/$/, "")}/submit.php`;
url.search = "";
return url;
}