import { NextResponse, type NextRequest } from 'next/server'; import { createDb, skillQueries, skillReviewQueries } from '@skillhub/db'; import { shouldCountView, getOrSetCache, cacheKeys, cacheTTL } from '@/lib/cache'; // Create database connection const db = createDb(); /** * Get client IP from request headers * Handles various proxy headers (Cloudflare, nginx, etc.) */ function getClientIp(request: NextRequest): string { // Try various headers in order of preference const cfConnectingIp = request.headers.get('cf-connecting-ip'); if (cfConnectingIp) return cfConnectingIp; const xRealIp = request.headers.get('x-real-ip'); if (xRealIp) return xRealIp; const xForwardedFor = request.headers.get('x-forwarded-for'); if (xForwardedFor) { // x-forwarded-for can contain multiple IPs, take the first one return xForwardedFor.split(',')[0].trim(); } // Fallback to a default (shouldn't happen in production) return 'unknown'; } export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string[] }> } ) { try { const { id } = await params; const skillId = id.join('/'); // Get skill and latest review from database (cached 1h) const [skill, review] = await Promise.all([ getOrSetCache( cacheKeys.skill(skillId), cacheTTL.skill, () => skillQueries.getById(db, skillId) ), getOrSetCache( cacheKeys.skillReview(skillId), cacheTTL.skill, () => skillReviewQueries.getLatestBySkillId(db, skillId) ), ]); if (!skill) { return NextResponse.json( { error: 'Skill not found' }, { status: 404 } ); } // Increment view count only on primary server (mirror DB is read-only) const isPrimary = process.env.IS_PRIMARY_SERVER !== 'false'; if (isPrimary) { const clientIp = getClientIp(request); const shouldCount = await shouldCountView(skillId, clientIp); if (shouldCount) { await skillQueries.incrementViews(db, skillId); } } return NextResponse.json({ id: skill.id, name: skill.name, description: skill.description, githubOwner: skill.githubOwner, githubRepo: skill.githubRepo, skillPath: skill.skillPath, branch: skill.branch, version: skill.version, license: skill.license, author: skill.author, homepage: skill.homepage, githubStars: skill.githubStars, githubForks: skill.githubForks, downloadCount: skill.downloadCount, viewCount: skill.viewCount, securityScore: skill.securityScore, securityStatus: skill.securityStatus, qualityScore: skill.qualityScore, isVerified: skill.isVerified, isFeatured: skill.isFeatured, isMalicious: skill.isMalicious ?? false, isDuplicate: skill.isDuplicate ?? false, isStale: skill.isStale ?? false, isDeprecated: skill.isDeprecated ?? false, isBlocked: skill.isBlocked ?? false, compatibility: skill.compatibility, triggers: skill.triggers, rawContent: skill.rawContent, sourceFormat: skill.sourceFormat || 'skill.md', reviewStatus: skill.reviewStatus, aiScore: skill.latestAiScore, createdAt: skill.createdAt, updatedAt: skill.updatedAt, indexedAt: skill.indexedAt, review: review ? { aiScore: review.aiScore, instructionQuality: review.instructionQuality, descriptionPrecision: review.descriptionPrecision, usefulness: review.usefulness, technicalSoundness: review.technicalSoundness, reviewNotes: review.reviewNotes, reviewer: review.reviewer, reviewedAt: review.reviewedAt, } : null, }); } catch (error) { console.error('Error fetching skill:', error); return NextResponse.json( { error: 'Failed to fetch skill' }, { status: 500 } ); } }