feat: 添加项目删除 API 接口和完整 API 文档
- 新增 DELETE /api/projects/:slug 接口,支持根据 slug 删除项目 - 删除操作会级联删除关联的外部链接和标签关系 - 新增 GET /api/projects/:slug 接口,支持查询项目详情 - 添加完整的 API 参考文档(docs/api-reference.md) - 修复 ProjectDetail 组件的 iframe 嵌入逻辑,添加可嵌入域名白名单 - 删除旧的数据摄入流程文档,由新 API 文档替代
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
/**
|
||||
* DELETE /api/projects/[slug]
|
||||
*
|
||||
* 根据项目的 slug 删除项目及其所有关联数据
|
||||
*
|
||||
* 由于数据库 schema 配置了 onDelete: Cascade,
|
||||
* 删除项目时会自动删除:
|
||||
* - 该项目的所有外部链接(ExternalLink)
|
||||
* - 该项目的所有标签关联(ProjectTag)
|
||||
*
|
||||
* 注意:Tag 本身不会被删除,只会删除项目与标签的关联关系
|
||||
*/
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { slug: string } }
|
||||
) {
|
||||
try {
|
||||
const slug = params.slug
|
||||
|
||||
// Verify API Key
|
||||
const apiKey = request.headers.get('x-api-key') || process.env.WEBHOOK_API_KEY
|
||||
const validApiKey = process.env.WEBHOOK_API_KEY
|
||||
|
||||
if (apiKey !== validApiKey) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
details: ['Invalid or missing API Key'],
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if project exists
|
||||
const existingProject = await prisma.project.findUnique({
|
||||
where: { slug },
|
||||
include: {
|
||||
links: true,
|
||||
tags: {
|
||||
include: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!existingProject) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Not Found',
|
||||
details: [`Project with slug "${slug}" not found`],
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Delete project (cascade delete will handle links and project_tags)
|
||||
await prisma.project.delete({
|
||||
where: { slug },
|
||||
})
|
||||
|
||||
console.warn(
|
||||
`[API] Deleted project "${existingProject.name}" (slug: ${slug}, id: ${existingProject.id})`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Project deleted successfully',
|
||||
data: {
|
||||
project: {
|
||||
id: existingProject.id,
|
||||
name: existingProject.name,
|
||||
nameEn: existingProject.nameEn,
|
||||
slug: existingProject.slug,
|
||||
},
|
||||
deleted: {
|
||||
linksCount: existingProject.links.length,
|
||||
tagsCount: existingProject.tags.length,
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[API] Error deleting project:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Internal server error',
|
||||
details: [error instanceof Error ? error.message : 'Unknown error'],
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/projects/[slug]
|
||||
*
|
||||
* 根据项目的 slug 获取项目详情
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { slug: string } }
|
||||
) {
|
||||
try {
|
||||
const slug = params.slug
|
||||
|
||||
const project = await prisma.project.findUnique({
|
||||
where: { slug },
|
||||
include: {
|
||||
links: true,
|
||||
tags: {
|
||||
include: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!project) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Not Found',
|
||||
details: [`Project with slug "${slug}" not found`],
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Transform response to match frontend structure
|
||||
const transformedProject = {
|
||||
...project,
|
||||
tags: project.tags.map((pt) => ({
|
||||
id: pt.tag.id,
|
||||
name: pt.tag.name,
|
||||
nameEn: pt.tag.nameEn,
|
||||
slug: pt.tag.slug,
|
||||
})),
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: transformedProject,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[API] Error fetching project:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Internal server error',
|
||||
details: [error instanceof Error ? error.message : 'Unknown error'],
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,41 @@ function isImageUrl(url: string): boolean {
|
||||
return imageExtensions.some(ext => lowerUrl.includes(ext))
|
||||
}
|
||||
|
||||
// Helper function to check if URL can be embedded in iframe
|
||||
// Many sites like GitHub, Google, etc. block iframe embedding via CSP
|
||||
function isEmbeddableUrl(url: string): boolean {
|
||||
const hostname = new URL(url).hostname.toLowerCase()
|
||||
|
||||
// Whitelist of domains that allow iframe embedding
|
||||
const embeddableDomains = [
|
||||
'youtube.com',
|
||||
'youtu.be',
|
||||
'vimeo.com',
|
||||
'player.vimeo.com',
|
||||
'drive.google.com',
|
||||
'docs.google.com',
|
||||
'www.figma.com',
|
||||
'codepen.io',
|
||||
'jsfiddle.net',
|
||||
'codesandbox.io',
|
||||
'stackblitz.com',
|
||||
'replit.com',
|
||||
'loom.com',
|
||||
'wistia.com',
|
||||
'brightcove.com',
|
||||
'dailymotion.com',
|
||||
'twitch.tv',
|
||||
'soundcloud.com',
|
||||
'spotify.com',
|
||||
'canva.com',
|
||||
'notion.so',
|
||||
'typeform.com',
|
||||
]
|
||||
|
||||
// Check if hostname matches any embeddable domain
|
||||
return embeddableDomains.some((domain) => hostname === domain || hostname.endsWith('.' + domain))
|
||||
}
|
||||
|
||||
export async function ProjectDetail({ project, locale }: ProjectDetailProps) {
|
||||
const t = await getTranslations('project')
|
||||
|
||||
@@ -124,14 +159,14 @@ export async function ProjectDetail({ project, locale }: ProjectDetailProps) {
|
||||
alt={`${displayName} demo`}
|
||||
className="w-full h-auto"
|
||||
/>
|
||||
) : (
|
||||
) : isEmbeddableUrl(project.source) ? (
|
||||
<iframe
|
||||
src={project.source}
|
||||
className="w-full aspect-video"
|
||||
title={`${displayName} demo`}
|
||||
allowFullScreen
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
|
||||
Reference in New Issue
Block a user