修复md导入问题
This commit is contained in:
@@ -1,7 +1,21 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireApiAuth } from "@/lib/api-auth";
|
||||
|
||||
export const runtime = "edge"; // Optional: Use edge runtime for lower latency
|
||||
// 禁止向内网地址发起请求,防止 SSRF 攻击
|
||||
const PRIVATE_HOST_PATTERNS = [
|
||||
/^localhost$/i,
|
||||
/^127\.\d+\.\d+\.\d+$/,
|
||||
/^10\.\d+\.\d+\.\d+$/,
|
||||
/^172\.(1[6-9]|2\d|3[01])\.\d+\.\d+$/,
|
||||
/^192\.168\.\d+\.\d+$/,
|
||||
/^0\.0\.0\.0$/,
|
||||
/^\[::1?\]$/,
|
||||
/^169\.254\.\d+\.\d+$/, // Link-local
|
||||
];
|
||||
|
||||
function isPrivateHost(hostname: string): boolean {
|
||||
return PRIVATE_HOST_PATTERNS.some((pattern) => pattern.test(hostname));
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const authError = await requireApiAuth();
|
||||
@@ -41,6 +55,10 @@ export async function POST(req: NextRequest) {
|
||||
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
||||
return NextResponse.json({ error: "Unsupported API base URL protocol" }, { status: 400 });
|
||||
}
|
||||
// SSRF 防护:禁止向内网地址发起请求
|
||||
if (isPrivateHost(parsed.hostname)) {
|
||||
return NextResponse.json({ error: "API base URL must not point to a private/internal address" }, { status: 400 });
|
||||
}
|
||||
normalizedBaseUrl = parsed.origin + parsed.pathname.replace(/\/$/, "");
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Invalid API base URL" }, { status: 400 });
|
||||
@@ -57,7 +75,7 @@ export async function POST(req: NextRequest) {
|
||||
body: JSON.stringify({
|
||||
model: typeof model === "string" && model.trim() ? model.trim() : "gpt-3.5-turbo",
|
||||
messages,
|
||||
stream: true, // Force streaming
|
||||
stream: true, // 强制使用流式
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -66,7 +84,7 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ error: `Upstream Error: ${res.statusText}`, details: errorText }, { status: res.status });
|
||||
}
|
||||
|
||||
// Return the stream directly
|
||||
// 直接返回上游 stream
|
||||
return new Response(res.body, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
@@ -80,3 +98,4 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,21 @@ import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { verifyPassword } from "@/lib/auth";
|
||||
import { createSessionToken, getSessionCookieName, getSessionTtlSeconds } from "@/lib/session";
|
||||
import { isRateLimited, getClientIp } from "@/lib/rate-limit";
|
||||
|
||||
// 登录速率限制:每个 IP 60 秒内最多 5 次尝试
|
||||
const LOGIN_MAX_ATTEMPTS = 5;
|
||||
const LOGIN_WINDOW_MS = 60 * 1000;
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const clientIp = getClientIp(req);
|
||||
if (isRateLimited(`login:${clientIp}`, LOGIN_MAX_ATTEMPTS, LOGIN_WINDOW_MS)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Too many login attempts, please try again later" },
|
||||
{ status: 429 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const { password, rememberMe, durationDays } = await req.json().catch(() => ({}));
|
||||
if (typeof password !== "string" || password.length === 0) {
|
||||
|
||||
@@ -3,13 +3,10 @@ import { prisma } from '@/lib/prisma';
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { requireApiAuth } from '@/lib/api-auth';
|
||||
import { cleanupAccidentalStandaloneInlineCode, markdownToImportHtml } from '@/lib/markdown-import';
|
||||
import { MAX_TITLE_LENGTH, safeParseTags, normalizeTags } from '@/lib/page-utils';
|
||||
|
||||
type PageRef = { id: string; parentId: string | null };
|
||||
|
||||
const MAX_TITLE_LENGTH = 200;
|
||||
const MAX_TAGS = 20;
|
||||
const MAX_TAG_LENGTH = 50;
|
||||
|
||||
function buildChildrenMap(pages: PageRef[]): Map<string | null, string[]> {
|
||||
const childrenMap = new Map<string | null, string[]>();
|
||||
for (const page of pages) {
|
||||
@@ -46,29 +43,6 @@ function collectDeleteOrder(rootId: string, pages: PageRef[]): string[] {
|
||||
return order;
|
||||
}
|
||||
|
||||
function safeParseTags(tags: string | null): string[] {
|
||||
if (!tags) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(tags);
|
||||
return Array.isArray(parsed) ? parsed.filter((t): t is string => typeof t === 'string') : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeTags(input: unknown): string[] {
|
||||
if (!Array.isArray(input)) return [];
|
||||
const unique = new Set<string>();
|
||||
for (const raw of input) {
|
||||
if (typeof raw !== 'string') continue;
|
||||
const normalized = raw.trim();
|
||||
if (!normalized || normalized.length > MAX_TAG_LENGTH) continue;
|
||||
unique.add(normalized);
|
||||
if (unique.size >= MAX_TAGS) break;
|
||||
}
|
||||
return Array.from(unique);
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
|
||||
@@ -2,33 +2,7 @@
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { requireApiAuth } from '@/lib/api-auth';
|
||||
import { cleanupAccidentalStandaloneInlineCode, markdownToImportHtml } from '@/lib/markdown-import';
|
||||
|
||||
const MAX_TITLE_LENGTH = 200;
|
||||
const MAX_TAGS = 20;
|
||||
const MAX_TAG_LENGTH = 50;
|
||||
|
||||
function safeParseTags(tags: string | null): string[] {
|
||||
if (!tags) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(tags);
|
||||
return Array.isArray(parsed) ? parsed.filter((t): t is string => typeof t === 'string') : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeTags(input: unknown): string[] {
|
||||
if (!Array.isArray(input)) return [];
|
||||
const unique = new Set<string>();
|
||||
for (const raw of input) {
|
||||
if (typeof raw !== 'string') continue;
|
||||
const normalized = raw.trim();
|
||||
if (!normalized || normalized.length > MAX_TAG_LENGTH) continue;
|
||||
unique.add(normalized);
|
||||
if (unique.size >= MAX_TAGS) break;
|
||||
}
|
||||
return Array.from(unique);
|
||||
}
|
||||
import { MAX_TITLE_LENGTH, safeParseTags, normalizeTags } from '@/lib/page-utils';
|
||||
|
||||
export async function GET() {
|
||||
const authError = await requireApiAuth();
|
||||
|
||||
@@ -153,6 +153,9 @@ export async function POST(req: NextRequest) {
|
||||
);
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// ⚠️ 注意:这是一个破坏性操作,会删除所有现有页面后重新导入。
|
||||
// TODO: 后续可在此添加自动备份逻辑(导出当前数据到临时文件),
|
||||
// 或在前端恢复前强制用户先手动备份。
|
||||
await tx.page.deleteMany();
|
||||
|
||||
const folderIdMap = new Map<string, string>();
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
/**
|
||||
* 检查系统是否已初始化(是否已设置密码)。
|
||||
*
|
||||
* 注意:此接口**不需要认证**,因为登录页面需要在用户未认证时
|
||||
* 调用此接口来判断是否需要显示"初始化密码"还是"登录"界面。
|
||||
*/
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const settings = await prisma.globalSettings.findUnique({
|
||||
|
||||
Reference in New Issue
Block a user