Files
agent-park/src/app/[locale]/projects/[id]/page.tsx
T
mzaxdandClaude 2a37a83072 fix: 修复代码审查发现的安全性和性能问题
主要修复:
- 安全性:API 密钥使用恒定时间比较防止时序攻击
- 安全性:URL 验证仅允许 http/https 协议,防止 XSS
- 安全性:移除 Markdown 的 rehypeRaw 插件增强 XSS 防护
- 安全性:Webhook 错误信息脱敏,生产环境不暴露敏感数据
- 性能:项目详情页使用并行数据获取
- 性能:Webhook 批量查询标签,避免 N+1 查询问题
- 类型安全:移除 any 类型,使用 Prisma 类型注解
- 类型安全:为 useProjects 函数添加返回类型
- 代码质量:创建 slug 工具函数统一 slug 生成逻辑
- 代码质量:搜索字符串添加最小长度限制(2字符)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-16 14:43:46 +08:00

94 lines
3.1 KiB
TypeScript

import { notFound } from 'next/navigation'
import { getTranslations } from 'next-intl/server'
import { getProjectBySlug, getProjects } from '@/hooks/useProjects'
import { ProjectDetail } from '@/components/project/ProjectDetail'
import { ProjectSidebar } from '@/components/project/ProjectSidebar'
import { RelatedProjects } from '@/components/project/RelatedProjects'
interface ProjectDetailPageProps {
params: Promise<{ locale: string; id: string }>
}
export default async function ProjectDetailPage({
params,
}: ProjectDetailPageProps) {
const resolvedParams = await params
const { locale, id } = resolvedParams
const tProject = await getTranslations('project')
// 并行获取项目和相关项目数据
const [project, relatedProjectsResult] = await Promise.all([
getProjectBySlug(id),
getProjects({ limit: 3 }),
])
if (!project) {
notFound()
}
const relatedProjects = relatedProjectsResult.projects
.filter(p => p.id !== project.id)
.filter(p => p.tags.some(t => project.tags.some(pt => pt.id === t.id)))
.slice(0, 3)
return (
<main className="min-h-screen bg-background-light dark:bg-background-dark text-text-light dark:text-text-dark font-body transition-colors duration-200">
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-12">
{/* Back Button */}
<div className="mb-8">
<a
className="inline-flex items-center text-sm font-display font-bold text-gray-500 hover:text-black dark:text-gray-400 dark:hover:text-white transition-colors group"
href={`/${locale}/projects`}
>
<span className="material-icons text-base mr-1 group-hover:-translate-x-1 transition-transform">
arrow_back
</span>
{tProject('backToProjects')}
</a>
</div>
{/* Main Grid Layout */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12">
{/* Main Content (8 columns) */}
<div className="lg:col-span-8">
<ProjectDetail project={project} locale={locale} />
</div>
{/* Sidebar (4 columns) */}
<aside className="lg:col-span-4">
<ProjectSidebar project={project} locale={locale} />
</aside>
</div>
{/* Related Projects Section */}
<RelatedProjects projects={relatedProjects} locale={locale} />
</div>
</main>
)
}
export async function generateMetadata({ params }: ProjectDetailPageProps) {
const resolvedParams = await params
const { id, locale } = resolvedParams
const tProject = await getTranslations('project')
const project = await getProjectBySlug(id)
if (!project) {
return {
title: tProject('notFound'),
}
}
const displayName = locale === 'en' && project.nameEn ? project.nameEn : project.name
const displayDescription =
locale === 'en' && project.descriptionEn ? project.descriptionEn : project.description
return {
title: `${displayName} - Agent Park`,
description: displayDescription,
}
}
export const revalidate = 300 // ISR: revalidate every 5 minutes