feat: separate subscription base url

This commit is contained in:
JetSprow
2026-04-29 13:46:10 +10:00
parent 68eac100f2
commit a0c1a28f5a
14 changed files with 80 additions and 26 deletions

View File

@@ -14,6 +14,7 @@ import { sendSmtpTestEmail } from "@/services/email";
const settingsSchema = z.object({
siteName: z.string().trim().min(1, "站点名称不能为空"),
siteUrl: z.string().trim().optional(),
subscriptionUrl: z.string().trim().optional(),
supportContact: z.string().trim().optional(),
maintenanceNotice: z.string().trim().optional(),
siteNotice: z.string().trim().optional(),
@@ -88,6 +89,7 @@ function buildSettingsUpdate(parsed: z.infer<typeof settingsSchema>, current: Aw
const next = {
siteName: parsed.siteName,
siteUrl: normalizeSiteUrl(parsed.siteUrl) || null,
subscriptionUrl: normalizeSiteUrl(parsed.subscriptionUrl) || null,
supportContact: parsed.supportContact || null,
maintenanceNotice: parsed.maintenanceNotice || null,
siteNotice: parsed.siteNotice || null,

View File

@@ -166,7 +166,7 @@ export function NodeActions({ node, siteUrl }: { node: NodeActionValue; siteUrl:
{!siteUrl && (
<p className="rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs leading-5 text-amber-700 dark:text-amber-200">
URL
</p>
)}
<p className="text-xs leading-5 text-muted-foreground">

View File

@@ -25,6 +25,7 @@ export default async function AdminSettingsPage() {
config={{
siteName: config.siteName,
siteUrl: config.siteUrl,
subscriptionUrl: config.subscriptionUrl,
supportContact: config.supportContact,
maintenanceNotice: config.maintenanceNotice,
siteNotice: config.siteNotice,

View File

@@ -14,6 +14,7 @@ import { getErrorMessage } from "@/lib/errors";
interface AppConfig {
siteName: string;
siteUrl: string | null;
subscriptionUrl: string | null;
supportContact: string | null;
maintenanceNotice: string | null;
siteNotice: string | null;
@@ -130,9 +131,14 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
<Input id="siteName" name="siteName" defaultValue={config.siteName} required />
</div>
<div className="space-y-2">
<Label htmlFor="siteUrl"> / URL</Label>
<Input id="siteUrl" name="siteUrl" defaultValue={config.siteUrl ?? ""} placeholder="https://example.com" />
<p className="text-xs leading-5 text-muted-foreground"> Agent </p>
<Label htmlFor="siteUrl"> URL</Label>
<Input id="siteUrl" name="siteUrl" defaultValue={config.siteUrl ?? ""} placeholder="https://panel.example.com" />
<p className="text-xs leading-5 text-muted-foreground"> Agent </p>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="subscriptionUrl"> URL</Label>
<Input id="subscriptionUrl" name="subscriptionUrl" defaultValue={config.subscriptionUrl ?? ""} placeholder="https://sub.example.com" />
<p className="text-xs leading-5 text-muted-foreground"> URL 使 sub 便 Cloudflare/WAF 访</p>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="supportContact"></Label>

View File

@@ -7,7 +7,7 @@ import { PageHeader, PageShell, SectionHeader } from "@/components/shared/page-s
import { SubscriptionDetailCards } from "@/components/subscriptions/subscription-detail-cards";
import { SubscriptionTimelineSection } from "@/components/subscriptions/subscription-timeline-section";
import { TrafficLogs } from "./_components/traffic-logs";
import { getSiteBaseUrl } from "@/services/site-url";
import { getSubscriptionBaseUrl } from "@/services/site-url";
import { ProxySubscriptionDetails } from "../_components/proxy-subscription-details";
import { StreamingCredentialCard } from "../streaming-credential-card";
import { getUserSubscriptionDetail } from "./subscription-detail-data";
@@ -30,7 +30,7 @@ export default async function UserSubscriptionDetailPage({
subscriptionId: id,
userId: session!.user.id,
}),
getSiteBaseUrl({ headers: requestHeaders }),
getSubscriptionBaseUrl({ headers: requestHeaders }),
]);
if (!data) {

View File

@@ -1,6 +1,6 @@
import { headers } from "next/headers";
import { prisma } from "@/lib/prisma";
import { getSiteBaseUrl } from "@/services/site-url";
import { getSubscriptionBaseUrl as resolveSubscriptionBaseUrl } from "@/services/site-url";
import {
getPlanTrafficPoolState,
type PlanTrafficPoolState,
@@ -22,7 +22,7 @@ export async function getUserSubscriptions(userId: string): Promise<Subscription
export async function getSubscriptionBaseUrl() {
const requestHeaders = await headers();
return getSiteBaseUrl({ headers: requestHeaders });
return resolveSubscriptionBaseUrl({ headers: requestHeaders });
}
export async function getTrafficPoolMap(subscriptions: SubscriptionRecord[]) {

View File

@@ -73,7 +73,7 @@ export async function POST(req: Request) {
const baseUrl = await getSiteBaseUrl({ headers: req.headers, requestUrl: req.url });
if (!baseUrl) {
return jsonError("请先在后台系统设置里配置站点域名", { status: 400 });
return jsonError("请先在后台系统设置里配置网站 URL", { status: 400 });
}
const result = await adapter.createPayment({
tradeNo,

View File

@@ -132,7 +132,7 @@ async function buildActionUrl(pathname: string, token: string, options: { header
allowRequestFallback: true,
});
if (!baseUrl) {
throw new Error("请先在系统设置中填写站点域名");
throw new Error("请先在系统设置中填写网站 URL");
}
const url = new URL(pathname, baseUrl);

View File

@@ -18,14 +18,14 @@ export function normalizeSiteUrl(raw: string | null | undefined): string | null
try {
url = new URL(withProtocol);
} catch {
throw new Error("站点域名格式不正确,请填写 https://example.com");
throw new Error("URL 格式不正确,请填写 https://example.com");
}
if (url.protocol !== "http:" && url.protocol !== "https:") {
throw new Error("站点域名仅支持 http:// 或 https://");
throw new Error("URL 仅支持 http:// 或 https://");
}
if (!url.hostname) {
throw new Error("站点域名不能为空");
throw new Error("URL 主机不能为空");
}
url.search = "";
@@ -79,6 +79,14 @@ export async function getConfiguredSiteUrl(db: DbClient = prisma): Promise<strin
return safeNormalizeSiteUrl(config.siteUrl) ?? safeNormalizeSiteUrl(process.env.NEXTAUTH_URL);
}
export async function getConfiguredSubscriptionUrl(db: DbClient = prisma): Promise<string | null> {
const config = await getAppConfig(db);
return safeNormalizeSiteUrl(config.subscriptionUrl)
?? safeNormalizeSiteUrl(process.env.SUBSCRIPTION_URL)
?? safeNormalizeSiteUrl(config.siteUrl)
?? safeNormalizeSiteUrl(process.env.NEXTAUTH_URL);
}
export async function getSiteBaseUrl(options: {
headers?: Headers;
requestUrl?: string;
@@ -93,3 +101,18 @@ export async function getSiteBaseUrl(options: {
options.headers ? getForwardedSiteUrl(options.headers) : null
) ?? getRequestOriginUrl(options.requestUrl) ?? "";
}
export async function getSubscriptionBaseUrl(options: {
headers?: Headers;
requestUrl?: string;
db?: DbClient;
allowRequestFallback?: boolean;
} = {}): Promise<string> {
const configured = await getConfiguredSubscriptionUrl(options.db ?? prisma);
if (configured) return configured;
if (!options.allowRequestFallback) return "";
return (
options.headers ? getForwardedSiteUrl(options.headers) : null
) ?? getRequestOriginUrl(options.requestUrl) ?? "";
}