fix: harden secrets and session checks

This commit is contained in:
JetSprow
2026-04-29 17:18:36 +10:00
parent 69be1d6fcc
commit 58fa4fefa4
44 changed files with 454 additions and 154 deletions

View File

@@ -6,16 +6,23 @@ import { requireAdmin } from "@/lib/require-auth";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { actorFromSession, recordAuditLog } from "@/services/audit";
import { encrypt } from "@/lib/crypto";
import { encrypt, isEncryptedValue } from "@/lib/crypto";
import { testAndSyncNodeInbounds } from "@/services/node-panel/sync-inbounds";
const nodeSchema = z.object({
const nodeBaseSchema = z.object({
name: z.string().trim().optional(),
panelUrl: z.string().trim().min(1, "3x-ui 面板地址必填"),
panelUsername: z.string().trim().min(1, "3x-ui 用户名必填"),
});
const createNodeSchema = nodeBaseSchema.extend({
panelPassword: z.string().trim().min(1, "3x-ui 密码必填"),
});
const updateNodeSchema = nodeBaseSchema.extend({
panelPassword: z.string().trim().optional(),
});
function normalizePanelUrl(raw: string): string {
try {
let value = raw.trim();
@@ -34,24 +41,26 @@ function normalizePanelUrl(raw: string): string {
}
}
function parseNodeData(formData: FormData) {
const raw = nodeSchema.parse(Object.fromEntries(formData));
function parseNodeData(formData: FormData, mode: "create" | "update") {
const raw = (mode === "create" ? createNodeSchema : updateNodeSchema)
.parse(Object.fromEntries(formData));
const panelUrl = normalizePanelUrl(raw.panelUrl);
const panel = new URL(panelUrl);
const panelPassword = raw.panelPassword?.trim();
const name = (raw.name || "").trim() || `节点-${panel.hostname}`;
return {
name,
panelUrl,
panelUsername: raw.panelUsername,
panelPassword: raw.panelPassword,
...(panelPassword ? { panelPassword: encrypt(panelPassword) } : {}),
panelType: "3x-ui",
};
}
export async function createNode(formData: FormData) {
const session = await requireAdmin();
const data = parseNodeData(formData);
const data = parseNodeData(formData, "create");
const node = await prisma.nodeServer.create({ data });
const result = await testAndSyncNodeInbounds(node);
@@ -72,7 +81,18 @@ export async function createNode(formData: FormData) {
export async function updateNode(id: string, formData: FormData) {
const session = await requireAdmin();
const data = parseNodeData(formData);
const data = parseNodeData(formData, "update");
if (!data.panelPassword) {
const existing = await prisma.nodeServer.findUnique({
where: { id },
select: { panelPassword: true },
});
if (existing?.panelPassword && !isEncryptedValue(existing.panelPassword)) {
data.panelPassword = encrypt(existing.panelPassword);
}
}
const node = await prisma.nodeServer.update({ where: { id }, data });
const result = await testAndSyncNodeInbounds(node);

View File

@@ -4,8 +4,10 @@ import { prisma } from "@/lib/prisma";
import { requireAdmin } from "@/lib/require-auth";
import { revalidatePath } from "next/cache";
import {
decryptPaymentConfigForUse,
normalizePaymentConfig,
parsePaymentConfig,
preparePaymentConfigForStorage,
} from "@/services/payment/catalog";
import { actorFromSession, recordAuditLog } from "@/services/audit";
import { z } from "zod";
@@ -18,11 +20,19 @@ export async function savePaymentConfig(
const session = await requireAdmin();
const normalizedConfig = normalizePaymentConfig(config);
let finalConfig = normalizedConfig as Record<string, string | number>;
const current = await prisma.paymentConfig.findUnique({
where: { provider },
select: { config: true },
});
const storageConfig = preparePaymentConfigForStorage(
provider,
normalizedConfig,
current?.config as Record<string, unknown> | undefined,
);
if (enabled) {
try {
finalConfig = parsePaymentConfig(provider, normalizedConfig) as Record<string, string | number>;
parsePaymentConfig(provider, decryptPaymentConfigForUse(provider, storageConfig));
} catch (error) {
if (error instanceof z.ZodError) {
const messages = error.issues.map((e) => e.message).join("");
@@ -32,7 +42,7 @@ export async function savePaymentConfig(
}
}
const jsonConfig = JSON.parse(JSON.stringify(finalConfig));
const jsonConfig = JSON.parse(JSON.stringify(storageConfig));
await prisma.paymentConfig.upsert({
where: { provider },

View File

@@ -8,7 +8,7 @@ import { requireAdmin } from "@/lib/require-auth";
import { actorFromSession, recordAuditLog } from "@/services/audit";
import { getAppConfig } from "@/services/app-config";
import { normalizeSiteUrl } from "@/services/site-url";
import { encrypt } from "@/lib/crypto";
import { encrypt, isEncryptedValue } from "@/lib/crypto";
import { getErrorMessage } from "@/lib/errors";
import { sendSmtpTestEmail } from "@/services/email";
@@ -100,6 +100,17 @@ function buildSettingsUpdate(parsed: z.infer<typeof settingsSchema>, current: Aw
const smtpPassword = parsed.smtpPassword?.trim()
? encrypt(parsed.smtpPassword.trim())
: current.smtpPassword;
const turnstileSiteKey = parsed.turnstileSiteKey || null;
const currentTurnstileSecret = current.turnstileSecretKey
? isEncryptedValue(current.turnstileSecretKey)
? current.turnstileSecretKey
: encrypt(current.turnstileSecretKey)
: null;
const turnstileSecretKey = parsed.turnstileSecretKey?.trim()
? encrypt(parsed.turnstileSecretKey.trim())
: turnstileSiteKey
? currentTurnstileSecret
: null;
const next = {
siteName: parsed.siteName,
@@ -150,8 +161,8 @@ function buildSettingsUpdate(parsed: z.infer<typeof settingsSchema>, current: Aw
inviteRewardEnabled: optionalBoolean(parsed.inviteRewardEnabled, current.inviteRewardEnabled),
inviteRewardRate: parsed.inviteRewardRate ?? Number(current.inviteRewardRate),
inviteRewardCouponId: parsed.inviteRewardCouponId || null,
turnstileSiteKey: parsed.turnstileSiteKey || null,
turnstileSecretKey: parsed.turnstileSecretKey || null,
turnstileSiteKey,
turnstileSecretKey,
smtpEnabled,
smtpHost: parsed.smtpHost || null,
smtpPort: parsed.smtpPort ?? current.smtpPort,