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

@@ -1,9 +1,8 @@
import { getServerSession } from "next-auth";
import { authOptions } from "./auth";
import { jsonError } from "./api-response";
import { getActiveSession } from "./require-auth";
export async function requireAdminApiSession() {
const session = await getServerSession(authOptions);
const session = await getActiveSession();
if (!session || session.user.role !== "ADMIN") {
return {
session: null,

View File

@@ -3,6 +3,7 @@ import CredentialsProvider from "next-auth/providers/credentials";
import bcrypt from "bcryptjs";
import { prisma } from "./prisma";
import { verifyTurnstile } from "./turnstile";
import { decryptIfEncrypted } from "./crypto";
export const authOptions: NextAuthOptions = {
providers: [
@@ -17,9 +18,12 @@ export const authOptions: NextAuthOptions = {
if (!credentials?.email || !credentials?.password) return null;
const config = await prisma.appConfig.findUnique({ where: { id: "default" } });
if (config?.turnstileSecretKey) {
const turnstileSecretKey = config?.turnstileSecretKey
? decryptIfEncrypted(config.turnstileSecretKey)
: "";
if (turnstileSecretKey) {
const token = credentials.turnstileToken;
if (!token || !(await verifyTurnstile(token, config.turnstileSecretKey))) {
if (!token || !(await verifyTurnstile(token, turnstileSecretKey))) {
return null;
}
}

View File

@@ -18,6 +18,22 @@ export function encrypt(text: string): string {
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
}
function isHexBytes(value: string) {
return value.length > 0 && value.length % 2 === 0 && /^[0-9a-f]+$/i.test(value);
}
export function isEncryptedValue(data: string): boolean {
const parts = data.split(":");
if (parts.length !== 3) return false;
const [ivHex, authTagHex, encryptedHex] = parts;
return ivHex.length === 32
&& authTagHex.length === 32
&& isHexBytes(ivHex)
&& isHexBytes(authTagHex)
&& isHexBytes(encryptedHex);
}
export function decrypt(data: string): string {
const parts = data.split(":");
if (parts.length !== 3) {
@@ -28,3 +44,7 @@ export function decrypt(data: string): string {
decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
return decipher.update(Buffer.from(encryptedHex, "hex")) + decipher.final("utf8");
}
export function decryptIfEncrypted(data: string): string {
return isEncryptedValue(data) ? decrypt(data) : data;
}

View File

@@ -1,9 +1,29 @@
import { getServerSession } from "next-auth";
import { authOptions } from "./auth";
import { prisma } from "./prisma";
import { getActiveSubscriptionRiskRestriction } from "@/services/subscription-risk-review";
export async function requireAdmin() {
export async function getActiveSession() {
const session = await getServerSession(authOptions);
if (!session?.user?.id) return null;
const user = await prisma.user.findUnique({
where: { id: session.user.id },
select: { id: true, email: true, name: true, role: true, status: true },
});
if (!user || user.status !== "ACTIVE") return null;
session.user.id = user.id;
session.user.email = user.email;
session.user.name = user.name;
session.user.role = user.role;
return session;
}
export async function requireAdmin() {
const session = await getActiveSession();
if (!session || session.user.role !== "ADMIN") {
throw new Error("无权限");
}
@@ -11,7 +31,7 @@ export async function requireAdmin() {
}
export async function requireAuth(options: { allowDuringRiskRestriction?: boolean } = {}) {
const session = await getServerSession(authOptions);
const session = await getActiveSession();
if (!session) {
throw new Error("未登录");
}