/** * Component: User Home Sections API Route * Documentation: documentation/features/home-sections.md * * Per-user configurable home page sections. * GET returns sections + next refresh time. * PUT saves full section config (delete-and-recreate in transaction). */ import { NextRequest, NextResponse } from 'next/server'; import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth'; import { prisma } from '@/lib/db'; import { z } from 'zod'; import { RMABLogger } from '@/lib/utils/logger'; const logger = RMABLogger.create('API.User.HomeSections'); const MAX_SECTIONS = 10; const VALID_SECTION_TYPES = ['popular', 'new_releases', 'category'] as const; const SectionSchema = z.object({ sectionType: z.enum(VALID_SECTION_TYPES), categoryId: z.string().optional().nullable(), categoryName: z.string().optional().nullable(), sortOrder: z.number().int().min(0), }); const PutBodySchema = z.object({ sections: z.array(SectionSchema).max(MAX_SECTIONS), }); /** * Create default home sections for a new user (Popular + New Releases). */ async function ensureDefaultSections(userId: string) { const existing = await prisma.userHomeSection.findMany({ where: { userId }, select: { id: true }, take: 1, }); if (existing.length > 0) return; await prisma.userHomeSection.createMany({ data: [ { userId, sectionType: 'popular', sortOrder: 0 }, { userId, sectionType: 'new_releases', sortOrder: 1 }, ], }); } /** * GET /api/user/home-sections * Returns the user's configured home sections + next scheduled refresh time. */ export async function GET(request: NextRequest) { return requireAuth(request, async (req: AuthenticatedRequest) => { try { if (!req.user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } await ensureDefaultSections(req.user.id); const sections = await prisma.userHomeSection.findMany({ where: { userId: req.user.id }, orderBy: { sortOrder: 'asc' }, }); // Get next refresh time from scheduled jobs let nextRefresh: string | null = null; try { const scheduledJob = await prisma.scheduledJob.findFirst({ where: { type: 'audible_refresh', enabled: true }, select: { nextRun: true }, }); nextRefresh = scheduledJob?.nextRun?.toISOString() || null; } catch { // Non-critical — just omit nextRefresh } return NextResponse.json({ success: true, sections: sections.map((s) => ({ id: s.id, sectionType: s.sectionType, categoryId: s.categoryId, categoryName: s.categoryName, sortOrder: s.sortOrder, })), nextRefresh, }); } catch (error) { logger.error('Failed to get home sections', { error: error instanceof Error ? error.message : String(error), }); return NextResponse.json( { error: 'FetchError', message: 'Failed to fetch home sections' }, { status: 500 } ); } }); } /** * PUT /api/user/home-sections * Replaces all home sections for the user (delete-and-recreate in transaction). * Validates: max 10 sections, no duplicate sections, category sections need categoryId. */ export async function PUT(request: NextRequest) { return requireAuth(request, async (req: AuthenticatedRequest) => { try { if (!req.user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const body = await req.json(); const { sections } = PutBodySchema.parse(body); // Validate category sections have categoryId for (const section of sections) { if (section.sectionType === 'category' && !section.categoryId) { return NextResponse.json( { error: 'ValidationError', message: 'Category sections require a categoryId' }, { status: 400 } ); } } // Check for duplicate section types (only one popular, one new_releases, unique categories) const seen = new Set(); for (const section of sections) { const key = section.sectionType === 'category' ? `category:${section.categoryId}` : section.sectionType; if (seen.has(key)) { return NextResponse.json( { error: 'ValidationError', message: `Duplicate section: ${key}` }, { status: 400 } ); } seen.add(key); } const userId = req.user.id; // Delete-and-recreate in a transaction await prisma.$transaction(async (tx) => { await tx.userHomeSection.deleteMany({ where: { userId } }); if (sections.length > 0) { await tx.userHomeSection.createMany({ data: sections.map((s, idx) => ({ userId, sectionType: s.sectionType, categoryId: s.sectionType === 'category' ? s.categoryId : null, categoryName: s.sectionType === 'category' ? s.categoryName : null, sortOrder: idx, })), }); } }); // Return the saved sections const saved = await prisma.userHomeSection.findMany({ where: { userId }, orderBy: { sortOrder: 'asc' }, }); logger.info(`User ${userId} updated home sections (${saved.length} sections)`); return NextResponse.json({ success: true, sections: saved.map((s) => ({ id: s.id, sectionType: s.sectionType, categoryId: s.categoryId, categoryName: s.categoryName, sortOrder: s.sortOrder, })), }); } catch (error) { logger.error('Failed to save home sections', { error: error instanceof Error ? error.message : String(error), }); if (error instanceof z.ZodError) { return NextResponse.json( { error: 'ValidationError', details: error.errors }, { status: 400 } ); } return NextResponse.json( { error: 'SaveError', message: 'Failed to save home sections' }, { status: 500 } ); } }); }