mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
fix: harden secrets and session checks
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user