init
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
import { createStableId } from "@/lib/password";
|
||||
import { requireAdminAccess } from "@/lib/session";
|
||||
import {
|
||||
getSubscriptionSettings,
|
||||
saveProxyGroups,
|
||||
saveSubscriptionSettings,
|
||||
} from "@/lib/store";
|
||||
|
||||
export async function saveSubscriptionSettingsAction(formData: FormData) {
|
||||
await requireAdminAccess();
|
||||
|
||||
await saveSubscriptionSettings({
|
||||
template: String(formData.get("template") ?? ""),
|
||||
extraProxyGroups: String(formData.get("extra_proxy_groups") ?? ""),
|
||||
profileName: String(formData.get("profile_name") ?? "").trim() || "FeieProxy",
|
||||
});
|
||||
|
||||
revalidatePath("/admin/subscriptions");
|
||||
}
|
||||
|
||||
export async function saveProxyGroupAction(formData: FormData) {
|
||||
await requireAdminAccess();
|
||||
|
||||
const settings = await getSubscriptionSettings();
|
||||
const id = String(formData.get("id") ?? "").trim() || createStableId();
|
||||
const name = String(formData.get("name") ?? "").trim();
|
||||
const memberSlugs = formData
|
||||
.getAll("member_slugs")
|
||||
.map((value) => String(value))
|
||||
.filter(Boolean);
|
||||
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const proxyGroups = settings.proxyGroups.filter((group) => group.id !== id);
|
||||
proxyGroups.push({
|
||||
id,
|
||||
name,
|
||||
type: "select",
|
||||
memberSlugs,
|
||||
});
|
||||
|
||||
await saveProxyGroups(proxyGroups);
|
||||
revalidatePath("/admin/subscriptions");
|
||||
}
|
||||
|
||||
export async function deleteProxyGroupAction(formData: FormData) {
|
||||
await requireAdminAccess();
|
||||
|
||||
const settings = await getSubscriptionSettings();
|
||||
const id = String(formData.get("id") ?? "").trim();
|
||||
await saveProxyGroups(settings.proxyGroups.filter((group) => group.id !== id));
|
||||
revalidatePath("/admin/subscriptions");
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import { getRequestOrigin } from "@/lib/request-origin";
|
||||
import { buildSubscriptionUrl, getDefaultClashTemplate } from "@/lib/subscription";
|
||||
import {
|
||||
getAdminMirrorUser,
|
||||
getSubscriptionSettings,
|
||||
listSubscriptionNodes,
|
||||
} from "@/lib/store";
|
||||
|
||||
import {
|
||||
deleteProxyGroupAction,
|
||||
saveProxyGroupAction,
|
||||
saveSubscriptionSettingsAction,
|
||||
} from "./actions";
|
||||
|
||||
export default async function AdminSubscriptionsPage() {
|
||||
const [settings, adminUser, nodes, origin] = await Promise.all([
|
||||
getSubscriptionSettings(),
|
||||
getAdminMirrorUser(),
|
||||
listSubscriptionNodes(),
|
||||
getRequestOrigin(),
|
||||
]);
|
||||
|
||||
return (
|
||||
<main className="page-stack">
|
||||
<section className="hero-panel">
|
||||
<div className="hero-copy">
|
||||
<span className="eyebrow">SUBSCRIPTIONS</span>
|
||||
<h2>订阅与模板</h2>
|
||||
<p className="page-lede">
|
||||
管理 Clash 基础模板、订阅文件名、代理分组以及管理员默认不限量订阅链接。
|
||||
</p>
|
||||
<div className="badge-row">
|
||||
<span className="status-pill status-pill--ok">动态 proxies</span>
|
||||
<span className="status-pill status-pill--ink">分组 {settings.proxyGroups.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hero-aside">
|
||||
<div className="stat-tile">
|
||||
<span>可用节点</span>
|
||||
<strong>{nodes.length}</strong>
|
||||
</div>
|
||||
<div className="stat-tile">
|
||||
<span>默认主组</span>
|
||||
<strong>{settings.profileName}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="panel-card">
|
||||
<div className="section-title">
|
||||
<h3>管理员订阅</h3>
|
||||
</div>
|
||||
{adminUser?.subscription_token ? (
|
||||
<div className="instruction-block no-margin">
|
||||
<p className="muted mono break-all">
|
||||
{buildSubscriptionUrl(adminUser.subscription_token, origin)}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="muted">管理员订阅链接暂不可用。</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="panel-card">
|
||||
<div className="section-title">
|
||||
<h3>Proxy Groups</h3>
|
||||
</div>
|
||||
<div className="stack-lg">
|
||||
{settings.proxyGroups.map((group) => (
|
||||
<article key={group.id} className="instruction-block">
|
||||
<form action={saveProxyGroupAction} className="stack-md">
|
||||
<input type="hidden" name="id" value={group.id} />
|
||||
<label className="field">
|
||||
<span>组名</span>
|
||||
<input name="name" defaultValue={group.name} required />
|
||||
</label>
|
||||
<div className="checkbox-grid">
|
||||
{nodes.map((node) => (
|
||||
<label key={`${group.id}-${node.slug}`} className="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="member_slugs"
|
||||
value={node.slug}
|
||||
defaultChecked={group.memberSlugs.includes(node.slug)}
|
||||
/>
|
||||
<span>{node.client_name || node.name}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className="action-row">
|
||||
<button className="ghost-button" type="submit">
|
||||
保存分组
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<form action={deleteProxyGroupAction}>
|
||||
<input type="hidden" name="id" value={group.id} />
|
||||
<button className="danger-button" type="submit">
|
||||
删除分组
|
||||
</button>
|
||||
</form>
|
||||
</article>
|
||||
))}
|
||||
|
||||
<article className="instruction-block">
|
||||
<form action={saveProxyGroupAction} className="stack-md">
|
||||
<label className="field">
|
||||
<span>新建分组名</span>
|
||||
<input name="name" placeholder="ChatGPT" required />
|
||||
</label>
|
||||
<div className="checkbox-grid">
|
||||
{nodes.map((node) => (
|
||||
<label key={`new-${node.slug}`} className="checkbox">
|
||||
<input type="checkbox" name="member_slugs" value={node.slug} />
|
||||
<span>{node.client_name || node.name}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<button className="primary-button" type="submit">
|
||||
新建分组
|
||||
</button>
|
||||
</form>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="panel-card">
|
||||
<div className="section-title">
|
||||
<h3>Clash 模板</h3>
|
||||
</div>
|
||||
<form action={saveSubscriptionSettingsAction} className="stack-lg">
|
||||
<label className="field">
|
||||
<span>订阅文件名 / 默认主组名</span>
|
||||
<input name="profile_name" defaultValue={settings.profileName} required />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>基础模板</span>
|
||||
<textarea
|
||||
name="template"
|
||||
rows={18}
|
||||
defaultValue={settings.template || getDefaultClashTemplate()}
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>附加 proxy-groups YAML</span>
|
||||
<textarea
|
||||
name="extra_proxy_groups"
|
||||
rows={12}
|
||||
defaultValue={settings.extraProxyGroups}
|
||||
placeholder={` - name: ChatGPT\n type: select\n proxies: ["US 01 - hy2", "JP 01 - hy2"]`}
|
||||
/>
|
||||
</label>
|
||||
<button className="primary-button" type="submit">
|
||||
保存订阅模板
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user