import { NextRequest, NextResponse } from 'next/server'; import { prisma, projectService } from '@/lib/prisma'; import { analyticsCache } from '@/lib/redis'; import { requireSessionAuth, checkRateLimit, getRateLimitHeaders } from '@/lib/auth'; export async function GET(request: NextRequest) { try { // Rate limiting - more generous for admin dashboard const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'; if (!checkRateLimit(ip, 20, 60000)) { // 20 requests per minute return new NextResponse( JSON.stringify({ error: 'Rate limit exceeded' }), { status: 429, headers: { 'Content-Type': 'application/json', ...getRateLimitHeaders(ip, 20, 60000) } } ); } // Admin-only endpoint: require explicit admin header AND a valid signed session token const isAdminRequest = request.headers.get('x-admin-request') === 'true'; if (!isAdminRequest) return NextResponse.json({ error: 'Admin access required' }, { status: 403 }); const authError = requireSessionAuth(request); if (authError) return authError; // Check cache first (but allow bypass with cache-bust parameter) const url = new URL(request.url); const bypassCache = url.searchParams.get('nocache') === 'true'; if (!bypassCache) { const cachedStats = await analyticsCache.getOverallStats(); if (cachedStats) { return NextResponse.json(cachedStats); } } // Get analytics data const projectsResult = await projectService.getAllProjects(); const projects = projectsResult.projects || projectsResult; const performanceStats = await projectService.getPerformanceStats(); const since = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // Use DB aggregation instead of loading every PageView row into memory const [totalViews, sessionsByIp, viewsByProjectRows] = await Promise.all([ prisma.pageView.count({ where: { timestamp: { gte: since } } }), prisma.pageView.groupBy({ by: ['ip'], where: { timestamp: { gte: since }, ip: { not: null }, }, _count: { _all: true }, _min: { timestamp: true }, _max: { timestamp: true }, }), prisma.pageView.groupBy({ by: ['projectId'], where: { timestamp: { gte: since }, projectId: { not: null }, }, _count: { _all: true }, }), ]); const totalSessions = sessionsByIp.length; const bouncedSessions = sessionsByIp.filter(s => (s as unknown as { _count?: { _all?: number } })._count?._all === 1).length; const bounceRate = totalSessions > 0 ? Math.round((bouncedSessions / totalSessions) * 100) : 0; const sessionDurationsMs = sessionsByIp .map(s => { const count = (s as unknown as { _count?: { _all?: number } })._count?._all ?? 0; if (count < 2) return 0; const minTs = (s as unknown as { _min?: { timestamp?: Date | null } })._min?.timestamp; const maxTs = (s as unknown as { _max?: { timestamp?: Date | null } })._max?.timestamp; if (!minTs || !maxTs) return 0; return maxTs.getTime() - minTs.getTime(); }) .filter(ms => ms > 0); const avgSessionDuration = sessionDurationsMs.length > 0 ? Math.round(sessionDurationsMs.reduce((a, b) => a + b, 0) / sessionDurationsMs.length / 1000) : 0; const totalUsers = totalSessions; const viewsByProject = viewsByProjectRows.reduce((acc, row) => { const projectId = row.projectId as number | null; if (projectId != null) { acc[projectId] = (row as unknown as { _count?: { _all?: number } })._count?._all ?? 0; } return acc; }, {} as Record); // Calculate analytics metrics const analytics = { overview: { totalProjects: projects.length, publishedProjects: projects.filter(p => p.published).length, featuredProjects: projects.filter(p => p.featured).length, totalViews, // Real views from PageView table totalLikes: 0, // Not implemented - no like buttons totalShares: 0, // Not implemented - no share buttons avgLighthouse: (() => { // Only calculate if we have real performance data (not defaults) const projectsWithPerf = projects.filter(p => { const perf = (p.performance as Record) || {}; const lighthouse = perf.lighthouse as number || 0; return lighthouse > 0; // Only count projects with actual performance data }); return projectsWithPerf.length > 0 ? Math.round(projectsWithPerf.reduce((sum, p) => sum + ((p.performance as Record)?.lighthouse as number || 0), 0) / projectsWithPerf.length) : 0; })() }, projects: projects.map(project => ({ id: project.id, title: project.title, category: project.category, difficulty: project.difficulty, views: viewsByProject[project.id] || 0, // Only real views from PageView table likes: 0, // Not implemented shares: 0, // Not implemented lighthouse: (() => { const perf = (project.performance as Record) || {}; const score = perf.lighthouse as number || 0; return score > 0 ? score : 0; // Only return if we have real data })(), published: project.published, featured: project.featured, createdAt: project.createdAt, updatedAt: project.updatedAt })), categories: performanceStats.byCategory, difficulties: performanceStats.byDifficulty, performance: { avgLighthouse: (() => { const projectsWithPerf = projects.filter(p => { const perf = (p.performance as Record) || {}; return (perf.lighthouse as number || 0) > 0; }); return projectsWithPerf.length > 0 ? Math.round(projectsWithPerf.reduce((sum, p) => sum + ((p.performance as Record)?.lighthouse as number || 0), 0) / projectsWithPerf.length) : 0; })(), totalViews, // Real total views totalLikes: 0, totalShares: 0 }, metrics: { bounceRate, avgSessionDuration, pagesPerSession: totalSessions > 0 ? (totalViews / totalSessions).toFixed(1) : '0', newUsers: totalUsers, totalUsers } }; // Cache the results await analyticsCache.setOverallStats(analytics); return NextResponse.json(analytics); } catch (error) { console.error('Analytics dashboard error:', error); return NextResponse.json( { error: 'Failed to fetch analytics data' }, { status: 500 } ); } }