ce439db449
主要改动: - API: 为 /api/pages 添加 lightweight 模式和分页支持 - 备份恢复: 增强 restore API 的错误处理和验证逻辑 - 工具函数: 新增 page-utils 辅助函数 - 会话管理: 优化 session 和 auth 相关逻辑 - 文档: 添加 CLAUDE.md 项目指南,包含编辑器配置注意事项 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
137 lines
5.3 KiB
TypeScript
137 lines
5.3 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { prisma } from '@/lib/prisma';
|
|
import { requireApiAuth } from '@/lib/api-auth';
|
|
import { cleanupAccidentalStandaloneInlineCode, markdownToImportHtml } from '@/lib/markdown-import';
|
|
import { MAX_TITLE_LENGTH, safeParseTags, normalizeTags } from '@/lib/page-utils';
|
|
|
|
export async function GET(request: NextRequest) {
|
|
const authError = await requireApiAuth();
|
|
if (authError) return authError;
|
|
|
|
try {
|
|
const searchParams = request.nextUrl.searchParams;
|
|
// lightweight 模式:不返回 content,用于侧边栏等场景
|
|
const lightweight = searchParams.get('lightweight') === 'true';
|
|
const page = parseInt(searchParams.get('page') || '0', 10);
|
|
const limit = parseInt(searchParams.get('limit') || '0', 10);
|
|
|
|
// 默认行为:返回所有页面(向后兼容)
|
|
const usePagination = limit > 0 && limit <= 1000 && page > 0;
|
|
|
|
if (!usePagination) {
|
|
const pages = await prisma.page.findMany({
|
|
orderBy: [{ order: 'asc' }, { createdAt: 'desc' }],
|
|
});
|
|
const parsedPages = pages.map((p) => ({
|
|
...p,
|
|
content: lightweight ? '' : p.content,
|
|
tags: safeParseTags(p.tags),
|
|
}));
|
|
return NextResponse.json(parsedPages);
|
|
}
|
|
|
|
// 分页查询
|
|
const skip = (page - 1) * limit;
|
|
const [pages, total] = await Promise.all([
|
|
prisma.page.findMany({
|
|
skip,
|
|
take: limit,
|
|
orderBy: [{ order: 'asc' }, { createdAt: 'desc' }],
|
|
}),
|
|
prisma.page.count(),
|
|
]);
|
|
|
|
const parsedPages = pages.map((p) => ({
|
|
...p,
|
|
content: lightweight ? '' : p.content,
|
|
tags: safeParseTags(p.tags),
|
|
}));
|
|
|
|
return NextResponse.json({
|
|
data: parsedPages,
|
|
pagination: {
|
|
page,
|
|
limit,
|
|
total,
|
|
totalPages: Math.ceil(total / limit),
|
|
},
|
|
});
|
|
} catch {
|
|
return NextResponse.json({ error: 'Error fetching pages' }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
const authError = await requireApiAuth();
|
|
if (authError) return authError;
|
|
|
|
try {
|
|
const body = await request.json().catch(() => null);
|
|
if (!body || typeof body !== 'object') {
|
|
return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });
|
|
}
|
|
|
|
const rawTitle = typeof body.title === 'string' ? body.title.trim() : '';
|
|
const title = rawTitle || '无标题';
|
|
if (title.length > MAX_TITLE_LENGTH) {
|
|
return NextResponse.json({ error: 'Title too long' }, { status: 400 });
|
|
}
|
|
|
|
const type = body.type === 'folder' ? 'folder' : 'file';
|
|
const importMarkdown = typeof body.importMarkdown === 'string' ? body.importMarkdown : null;
|
|
const rawContent = typeof body.content === 'string' ? body.content : '';
|
|
const content = cleanupAccidentalStandaloneInlineCode(
|
|
importMarkdown !== null ? markdownToImportHtml(importMarkdown) : rawContent
|
|
);
|
|
if (importMarkdown !== null && process.env.NODE_ENV !== 'production') {
|
|
const lines = importMarkdown.replace(/\r\n?/g, '\n').split('\n');
|
|
const fenceLines = lines
|
|
.map((line: string, idx: number) => (line.trimStart().startsWith('```') || line.trimStart().startsWith('~~~') ? idx + 1 : -1))
|
|
.filter((n: number) => n > 0);
|
|
const preEnd = content.indexOf('</code></pre>');
|
|
console.info(`[import-md][POST] title="${title}" fences=${fenceLines.join(',') || 'none'} preEnd=${preEnd} len=${content.length}`);
|
|
}
|
|
const parentId = typeof body.parentId === 'string' ? body.parentId : null;
|
|
const tags = normalizeTags(body.tags);
|
|
const icon = typeof body.icon === 'string' ? body.icon : null;
|
|
const isLocked = typeof body.isLocked === 'boolean' ? body.isLocked : false;
|
|
const requestedOrder = Number.isInteger(body.order) ? body.order : undefined;
|
|
|
|
if (parentId) {
|
|
const parent = await prisma.page.findUnique({
|
|
where: { id: parentId },
|
|
select: { id: true, type: true },
|
|
});
|
|
if (!parent || parent.type !== 'folder') {
|
|
return NextResponse.json({ error: 'Parent folder not found' }, { status: 400 });
|
|
}
|
|
}
|
|
|
|
const page = await prisma.page.create({
|
|
data: {
|
|
title,
|
|
content,
|
|
tags: JSON.stringify(tags),
|
|
parentId,
|
|
type,
|
|
order: await (async () => {
|
|
if (requestedOrder !== undefined) return requestedOrder;
|
|
const lastPage = await prisma.page.findFirst({
|
|
where: { parentId },
|
|
orderBy: { order: 'desc' },
|
|
});
|
|
return (lastPage?.order ?? -1) + 1;
|
|
})(),
|
|
icon,
|
|
isLocked,
|
|
},
|
|
});
|
|
return NextResponse.json({ ...page, tags: safeParseTags(page.tags) });
|
|
} catch {
|
|
return NextResponse.json({ error: 'Error creating page' }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
|
|
|