This commit is contained in:
2026-04-14 18:05:32 +08:00
parent 68457c9fbd
commit 6378ab4994
40 changed files with 10488 additions and 113 deletions
@@ -0,0 +1,28 @@
import { NextResponse } from "next/server";
import { getOriginFromRequest } from "@/lib/request-origin";
import { requireAdminAccess } from "@/lib/session";
import { buildNodeHy2Config } from "@/lib/subscription";
import { getNodeBySlug } from "@/lib/store";
export async function GET(
request: Request,
context: RouteContext<"/api/admin/nodes/[slug]/config">,
) {
await requireAdminAccess();
const { slug } = await context.params;
const node = await getNodeBySlug(slug);
if (!node) {
return new NextResponse("node not found", { status: 404 });
}
return new NextResponse(buildNodeHy2Config(node, getOriginFromRequest(request)), {
status: 200,
headers: {
"Content-Type": "application/x-yaml; charset=utf-8",
"Content-Disposition": `attachment; filename="${slug}.config.yaml"`,
},
});
}
+134
View File
@@ -0,0 +1,134 @@
import { getUserByUsername, writeAuthAudit } from "@/lib/store";
import { parseHy2Userpass, verifyNodeToken, evaluateUserForAuth } from "@/lib/hy2";
import { verifyPassword } from "@/lib/password";
type AuthRequest = {
addr?: string;
auth?: string;
tx?: number;
};
function deny(id = "") {
return Response.json({ ok: false, id });
}
export async function POST(
request: Request,
context: RouteContext<"/api/hy2/nodes/[slug]/auth">,
) {
const { slug } = await context.params;
const requestUrl = new URL(request.url);
const token =
requestUrl.searchParams.get("token") ||
request.headers.get("authorization")?.replace(/^Bearer\s+/i, "") ||
request.headers.get("x-node-token");
const nodeResult = await verifyNodeToken(slug, token);
if (!nodeResult.ok) {
await writeAuthAudit({
nodeId: nodeResult.node?.id ?? null,
ok: false,
reason: nodeResult.reason,
});
return deny();
}
const node = nodeResult.node;
if (!node) {
return deny();
}
let payload: AuthRequest;
try {
payload = (await request.json()) as AuthRequest;
} catch {
await writeAuthAudit({
nodeId: node.id,
addr: null,
requestedTx: null,
ok: false,
reason: "bad_json",
});
return deny();
}
if (!payload.auth) {
await writeAuthAudit({
nodeId: node.id,
addr: payload.addr ?? null,
requestedTx: payload.tx ?? null,
ok: false,
reason: "missing_auth",
});
return deny();
}
const parsed = parseHy2Userpass(payload.auth);
if (!parsed) {
await writeAuthAudit({
nodeId: node.id,
addr: payload.addr ?? null,
requestedTx: payload.tx ?? null,
ok: false,
reason: "bad_auth_format",
});
return deny();
}
const user = await getUserByUsername(parsed.username);
if (!user) {
await writeAuthAudit({
nodeId: node.id,
username: parsed.username,
addr: payload.addr ?? null,
requestedTx: payload.tx ?? null,
ok: false,
reason: "user_not_found",
});
return deny();
}
const passwordOk = await verifyPassword(parsed.password, user.password_hash);
if (!passwordOk) {
await writeAuthAudit({
nodeId: node.id,
userId: user.id,
authId: user.auth_id,
username: user.username,
addr: payload.addr ?? null,
requestedTx: payload.tx ?? null,
ok: false,
reason: "password_invalid",
});
return deny();
}
const access = evaluateUserForAuth(user);
if (!access.ok) {
await writeAuthAudit({
nodeId: node.id,
userId: user.id,
authId: user.auth_id,
username: user.username,
addr: payload.addr ?? null,
requestedTx: payload.tx ?? null,
ok: false,
reason: access.reason,
});
return deny();
}
await writeAuthAudit({
nodeId: node.id,
userId: user.id,
authId: user.auth_id,
username: user.username,
addr: payload.addr ?? null,
requestedTx: payload.tx ?? null,
ok: true,
reason: "ok",
});
return Response.json({ ok: true, id: user.auth_id });
}
@@ -0,0 +1,38 @@
import { buildSubscriptionConfig, buildSubscriptionHeaders } from "@/lib/subscription";
import {
getSubscriptionSettings,
getUserBySubscriptionToken,
listSubscriptionNodes,
} from "@/lib/store";
export async function GET(
_request: Request,
context: RouteContext<"/api/subscription/[token]/clash">,
) {
const { token } = await context.params;
const user = await getUserBySubscriptionToken(token);
if (!user || !user.enabled) {
return new Response("subscription not found", { status: 404 });
}
const [nodes, settings] = await Promise.all([
listSubscriptionNodes(),
getSubscriptionSettings(),
]);
let body: string;
try {
body = buildSubscriptionConfig({ user, nodes, settings });
} catch {
return new Response("subscription secret unavailable", { status: 409 });
}
const profileName = user.subscription_label || settings.profileName || user.username;
return new Response(body, {
status: 200,
headers: buildSubscriptionHeaders(user, profileName),
});
}