import type { FastifyPluginAsync } from 'fastify'; import { z } from 'zod'; import { prisma } from '../db.js'; import { requireAuth } from '../plugins/session.js'; export const lmsRoutes: FastifyPluginAsync = async (app) => { // Public route to fetch learning paths app.get('/api/v1/lms/paths', async () => { const paths = await prisma.learningPath.findMany({ include: { modules: { orderBy: { order: 'asc' }, include: { articles: { include: { article: { select: { title: true, slug: true, readTime: true } } } } } } }, orderBy: { createdAt: 'desc' } }); return { paths }; }); app.get('/api/v1/lms/paths/:slug', async (req, reply) => { const { slug } = req.params as { slug: string }; const path = await prisma.learningPath.findUnique({ where: { slug }, include: { modules: { orderBy: { order: 'asc' }, include: { articles: { include: { article: { select: { id: true, title: true, slug: true, summary: true, readTime: true } } } }, quizzes: true } } } }); if (!path) return reply.code(404).send({ error: 'not_found' }); return { path }; }); // Authenticated routes app.register(async (authApp) => { authApp.addHook('preHandler', requireAuth()); // Get user's progress and certificates authApp.get('/api/v1/lms/me', async (req) => { const userId = req.session!.userId; const certificates = await prisma.userCertification.findMany({ where: { userId } }); const pathIds = certificates.map(c => c.pathId); const paths = await prisma.learningPath.findMany({ where: { id: { in: pathIds } } }); const resolvedCertificates = certificates.map(cert => ({ ...cert, path: paths.find(p => p.id === cert.pathId) })); return { certificates: resolvedCertificates }; }); // Submit a quiz result authApp.post('/api/v1/lms/quiz/:quizId/submit', async (req, reply) => { const { quizId } = req.params as { quizId: string }; const schema = z.object({ score: z.number().min(0).max(100) }); const parsed = schema.safeParse(req.body); if (!parsed.success) return reply.code(400).send({ error: 'invalid_input' }); const quiz = await prisma.quiz.findUnique({ where: { id: quizId }, include: { module: true } }); if (!quiz) return reply.code(404).send({ error: 'not_found' }); const passed = parsed.data.score >= 80; // Issue certificate if it's the final quiz and passed? // For simplicity in Phase 14 demo, we will issue a certificate if the score >= 80 on any path quiz if (passed && quiz.module.pathId) { const existing = await prisma.userCertification.findFirst({ where: { userId: req.session!.userId, pathId: quiz.module.pathId } }); if (existing) { await prisma.userCertification.update({ where: { id: existing.id }, data: { certificateUrl: `/certificates/${req.session!.userId}/${quiz.module.pathId}` } }); } else { await prisma.userCertification.create({ data: { userId: req.session!.userId, pathId: quiz.module.pathId, certificateUrl: `/certificates/${req.session!.userId}/${quiz.module.pathId}` } }); } } return { passed, score: parsed.data.score }; }); }); };