121 lines
3.3 KiB
TypeScript
121 lines
3.3 KiB
TypeScript
|
|
import { NextRequest, NextResponse } from "next/server";
|
||
|
|
import { prisma } from "@/lib/prisma";
|
||
|
|
import { requireAuth } from "@/lib/auth";
|
||
|
|
|
||
|
|
// GET /api/learn/courses/[slug] — course detail with modules and user progress
|
||
|
|
export async function GET(
|
||
|
|
request: NextRequest,
|
||
|
|
{ params }: { params: Promise<{ slug: string }> }
|
||
|
|
) {
|
||
|
|
try {
|
||
|
|
const auth = await requireAuth(request);
|
||
|
|
if (!auth) {
|
||
|
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||
|
|
}
|
||
|
|
|
||
|
|
const { slug } = await params;
|
||
|
|
|
||
|
|
const course = await prisma.learnCourse.findUnique({
|
||
|
|
where: { slug },
|
||
|
|
include: {
|
||
|
|
modules: {
|
||
|
|
orderBy: { order: "asc" },
|
||
|
|
},
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!course) {
|
||
|
|
return NextResponse.json(
|
||
|
|
{ error: "Course not found" },
|
||
|
|
{ status: 404 }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fetch user's enrollment for this course (if any)
|
||
|
|
let enrollment = await prisma.learnEnrollment.findUnique({
|
||
|
|
where: {
|
||
|
|
userId_courseId: {
|
||
|
|
userId: auth.userId,
|
||
|
|
courseId: course.id,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
// Auto-enroll premium users (check local user record, not JWT claims)
|
||
|
|
// The JWT's licenseTier may not match the local user's isPremium flag
|
||
|
|
if (!enrollment) {
|
||
|
|
const localUser = await prisma.user.findUnique({
|
||
|
|
where: { id: auth.userId },
|
||
|
|
select: { isPremium: true },
|
||
|
|
});
|
||
|
|
if (localUser?.isPremium) {
|
||
|
|
enrollment = await prisma.learnEnrollment.create({
|
||
|
|
data: {
|
||
|
|
userId: auth.userId,
|
||
|
|
courseId: course.id,
|
||
|
|
purchaseType: "free",
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fetch module progress for this enrollment (if enrolled)
|
||
|
|
let moduleProgressMap: Record<string, { completed: boolean; score: number | null; listened: boolean }> = {};
|
||
|
|
if (enrollment) {
|
||
|
|
const progresses = await prisma.learnModuleProgress.findMany({
|
||
|
|
where: { enrollmentId: enrollment.id },
|
||
|
|
});
|
||
|
|
for (const p of progresses) {
|
||
|
|
moduleProgressMap[p.moduleId] = {
|
||
|
|
completed: p.completed,
|
||
|
|
score: p.score,
|
||
|
|
listened: p.listened,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Build response
|
||
|
|
const modules = course.modules.map((mod) => ({
|
||
|
|
id: mod.id,
|
||
|
|
order: mod.order,
|
||
|
|
title: mod.title,
|
||
|
|
slug: mod.slug,
|
||
|
|
keyTakeaway: mod.keyTakeaway,
|
||
|
|
duration: mod.duration,
|
||
|
|
content: mod.content,
|
||
|
|
quizData: mod.quizData ? JSON.parse(mod.quizData) : null,
|
||
|
|
progress: moduleProgressMap[mod.id] ?? null,
|
||
|
|
}));
|
||
|
|
|
||
|
|
return NextResponse.json({
|
||
|
|
course: {
|
||
|
|
id: course.id,
|
||
|
|
slug: course.slug,
|
||
|
|
title: course.title,
|
||
|
|
author: course.author,
|
||
|
|
description: course.description,
|
||
|
|
imageUrl: course.imageUrl,
|
||
|
|
moduleCount: course.moduleCount,
|
||
|
|
totalMinutes: course.totalMinutes,
|
||
|
|
difficulty: course.difficulty,
|
||
|
|
giteaPath: course.giteaPath,
|
||
|
|
},
|
||
|
|
modules,
|
||
|
|
enrollment: enrollment
|
||
|
|
? {
|
||
|
|
id: enrollment.id,
|
||
|
|
purchaseType: enrollment.purchaseType,
|
||
|
|
completed: enrollment.completed,
|
||
|
|
progress: enrollment.progress,
|
||
|
|
}
|
||
|
|
: null,
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
console.error("GET /api/learn/courses/[slug] error:", error);
|
||
|
|
return NextResponse.json(
|
||
|
|
{ error: "Internal server error" },
|
||
|
|
{ status: 500 }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|