From 2fc1b2517ae2f8fdeed9be607fe4a16e75c6ae49 Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:20:16 +0330 Subject: [PATCH] Import source: app/backend/src/routes/lms Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality. --- app/backend/src/routes/lms.ts | 96 +++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 app/backend/src/routes/lms.ts diff --git a/app/backend/src/routes/lms.ts b/app/backend/src/routes/lms.ts new file mode 100644 index 0000000..7bb3fa3 --- /dev/null +++ b/app/backend/src/routes/lms.ts @@ -0,0 +1,96 @@ +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 }; + }); + }); +};