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.
This commit is contained in:
@@ -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 };
|
||||
});
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user