mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
chore: relocate backend into apps/api ahead of monorepo merge
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
const router = require('express').Router();
|
||||
const ctrl = require('../../controllers/admin/achievements.controller');
|
||||
|
||||
// Auth + requireAdmin applied by admin.routes.js
|
||||
|
||||
router.get ('/', ctrl.getAchievements);
|
||||
router.post ('/', ctrl.createAchievement);
|
||||
router.get ('/:id', ctrl.getAchievement);
|
||||
router.put ('/:id', ctrl.updateAchievement);
|
||||
router.delete('/:id', ctrl.deleteAchievement);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,79 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: admin.routes.js
|
||||
* Type of Program: Router
|
||||
* Description: Protected routes accessible by admin only — full control.
|
||||
*
|
||||
* Route Map:
|
||||
* Dashboard : GET /api/admin/dashboard
|
||||
* Users : /api/admin/users/*
|
||||
* Groups : /api/admin/groups/*
|
||||
* Assets : /api/admin/assets/*
|
||||
*
|
||||
* Guards: authenticate → requireAdmin() → adminLimiter
|
||||
*
|
||||
* Author: rgrgogu and Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Oct. 6, 2025
|
||||
* Date Modified: May 5, 2026
|
||||
* Latest: May 19, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
const { authenticate } = require('../../middleware/auth.middleware');
|
||||
const { requireAdmin } = require('../../middleware/rbac.middleware');
|
||||
const { adminLimiter } = require('../../middleware/rateLimiter.middleware');
|
||||
|
||||
const dashboardRoutes = require('./dashboard.routes');
|
||||
const profileRoutes = require('./profile.routes');
|
||||
const usersRoutes = require('./users.routes');
|
||||
const groupsRoutes = require('./groups.routes');
|
||||
const assetsRoutes = require('./assets.routes');
|
||||
const coursesRoutes = require('./courses.routes');
|
||||
const unitsRoutes = require('./units.routes');
|
||||
const lessonsRoutes = require('./lessons.routes');
|
||||
const taskRoutes = require('./task.routes');
|
||||
const tiersRoutes = require('./tiers.routes');
|
||||
const tierPoliciesRoutes = require('./tier_policies.routes');
|
||||
const tierCategoriesRoutes = require('./tier_categories.routes');
|
||||
const categoriesRoutes = require('./categories.routes');
|
||||
const productsRoutes = require('./products.routes');
|
||||
const advertisementRoutes = require('./advertisements.routes');
|
||||
const notificationRoutes = require('./notifications.routes');
|
||||
const notificationBroadcastRoutes = require('./notificationBroadcasts.routes');
|
||||
const notificationSettingsRoutes = require('./notificationSettings.routes');
|
||||
const mediaRoutes = require('./media.routes');
|
||||
const achievementsRoutes = require('./achievements.routes');
|
||||
const activityCtrl = require('../../controllers/admin/user_activity.controller');
|
||||
|
||||
// ── Guards — applied to ALL admin routes ──────────────────────────────────────
|
||||
router.use(authenticate, requireAdmin(), adminLimiter);
|
||||
|
||||
// ── Mount ─────────────────────────────────────────────────────────────────────
|
||||
router.use('/dashboard', dashboardRoutes);
|
||||
router.use('/profile', profileRoutes);
|
||||
router.use('/users', usersRoutes);
|
||||
router.use('/groups', groupsRoutes);
|
||||
router.use('/assets', assetsRoutes);
|
||||
router.use('/courses', coursesRoutes);
|
||||
router.use('/units', unitsRoutes); // standalone Unit library (junction revamp)
|
||||
router.use('/lessons', lessonsRoutes); // standalone Lesson library (junction revamp)
|
||||
router.use('/task-lists', taskRoutes);
|
||||
router.use('/tiers/categories', tierCategoriesRoutes);
|
||||
router.use('/tiers', tiersRoutes);
|
||||
router.use('/tier-policies', tierPoliciesRoutes);
|
||||
router.use('/categories', categoriesRoutes);
|
||||
router.use('/products', productsRoutes);
|
||||
router.use('/advertisements', advertisementRoutes);
|
||||
router.use('/notifications', notificationRoutes);
|
||||
router.use('/notification-broadcasts', notificationBroadcastRoutes);
|
||||
router.use('/notification-settings', notificationSettingsRoutes);
|
||||
// Announcements (alias routes for notification broadcasts/settings)
|
||||
router.use('/announcements', notificationBroadcastRoutes);
|
||||
router.use('/announcement-settings', notificationSettingsRoutes);
|
||||
router.use('/media', mediaRoutes);
|
||||
router.use('/achievements', achievementsRoutes);
|
||||
|
||||
// ── Activity feed — global ────────────────────────────────────────────────────
|
||||
router.get('/activity', activityCtrl.getActivity);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,25 @@
|
||||
// routes/admin/advertisements.routes.js
|
||||
const router = require('express').Router();
|
||||
const controller = require('../../controllers/admin/advertisements.controller');
|
||||
const { sensitiveOpsLimiter } = require('../../middleware/rateLimiter.middleware');
|
||||
|
||||
// ─── Static routes first ──────────────────────────────────────────────────────
|
||||
router.get('/archived', controller.getArchivedAdvertisements);
|
||||
router.get('/field-values', controller.getAdvertisementFieldValues);
|
||||
router.delete('/bulk', sensitiveOpsLimiter, controller.archiveAdvertisements);
|
||||
router.patch('/bulk-restore', controller.restoreAdvertisements);
|
||||
router.delete('/bulk/permanent', sensitiveOpsLimiter, controller.permanentlyDeleteAdvertisements);
|
||||
|
||||
// ─── Collection ───────────────────────────────────────────────────────────────
|
||||
router.get('/', controller.getAdvertisements);
|
||||
router.post('/', controller.createAdvertisement);
|
||||
|
||||
// ─── Dynamic routes last ──────────────────────────────────────────────────────
|
||||
router.get('/:advertisementId', controller.getAdvertisement);
|
||||
router.patch('/:advertisementId', sensitiveOpsLimiter, controller.updateAdvertisement);
|
||||
router.patch('/:advertisementId/reorder', sensitiveOpsLimiter, controller.reorderAdvertisement);
|
||||
router.patch('/:advertisementId/restore', sensitiveOpsLimiter, controller.restoreAdvertisement);
|
||||
router.delete('/:advertisementId', sensitiveOpsLimiter, controller.archiveAdvertisement);
|
||||
router.delete('/:advertisementId/permanent', sensitiveOpsLimiter, controller.permanentlyDeleteAdvertisement);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,36 @@
|
||||
// routes/admin/assets.routes.js
|
||||
|
||||
const router = require('express').Router();
|
||||
const controller = require('../../controllers/admin/assets.controller');
|
||||
const { sensitiveOpsLimiter } = require('../../middleware/rateLimiter.middleware');
|
||||
|
||||
// ─── Static routes first ──────────────────────────────────────────────────────
|
||||
router.get('/archived', controller.getArchivedAssets);
|
||||
router.get('/field-values', controller.getAssetFieldValues);
|
||||
router.delete('/bulk', sensitiveOpsLimiter, controller.archiveAssets);
|
||||
router.delete('/bulk/permanent', sensitiveOpsLimiter, controller.permanentlyDeleteAssets);
|
||||
router.patch('/bulk-restore', controller.restoreAssets);
|
||||
|
||||
// ─── Collection ───────────────────────────────────────────────────────────────
|
||||
router.get('/', controller.getAssets);
|
||||
// Presigned-upload flow: the browser PUTs file bytes straight to storage, this
|
||||
// backend never buffers them (see assets.controller.js). presign mints the
|
||||
// upload URL(s); the POST / body that follows is plain JSON (no multer here).
|
||||
router.post('/presign', controller.presignAssetUpload);
|
||||
// Above presign's single-PUT ceiling only (see s3.service.js's
|
||||
// MULTIPART_THRESHOLD) — the browser PUTs every part directly, then calls
|
||||
// complete/abort here. Plain JSON, no file bytes.
|
||||
router.post('/complete-multipart', controller.completeMultipartAssetUpload);
|
||||
router.post('/abort-multipart', controller.abortMultipartAssetUpload);
|
||||
router.post('/', controller.uploadAsset);
|
||||
|
||||
// ─── Dynamic routes last ──────────────────────────────────────────────────────
|
||||
router.get('/:assetId', controller.getAsset);
|
||||
// Replace flow also goes through presign + direct PUT (browser -> storage) —
|
||||
// this body is plain JSON (storage_key etc.), same as POST /. No multer here.
|
||||
router.patch('/:assetId', sensitiveOpsLimiter, controller.updateAsset);
|
||||
router.patch('/:assetId/restore', sensitiveOpsLimiter, controller.restoreAsset);
|
||||
router.delete('/:assetId', sensitiveOpsLimiter, controller.archiveAsset);
|
||||
router.delete('/:assetId/permanent', sensitiveOpsLimiter, controller.permanentlyDeleteAsset);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,17 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const ctrl = require('../../controllers/admin/categories.controller');
|
||||
|
||||
router.get ('/', ctrl.getCategories);
|
||||
router.post ('/', ctrl.createCategory);
|
||||
router.delete('/bulk', ctrl.bulkArchiveCategories);
|
||||
router.post ('/bulk-restore', ctrl.bulkRestoreCategories);
|
||||
router.delete('/bulk/permanent', ctrl.bulkPermanentlyDeleteCategories);
|
||||
router.get ('/:id/permanent-delete-impact', ctrl.getCategoryPermanentDeleteImpact);
|
||||
router.get ('/:id', ctrl.getCategory);
|
||||
router.put ('/:id', ctrl.updateCategory);
|
||||
router.delete('/:id', ctrl.archiveCategory);
|
||||
router.post ('/:id/restore', ctrl.restoreCategory);
|
||||
router.delete('/:id/permanent', ctrl.permanentlyDeleteCategory);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,203 @@
|
||||
const router = require("express").Router();
|
||||
const ctrl = require("../../controllers/admin/courses.controller");
|
||||
const progressCtrl = require("../../controllers/admin/course_reading_progress.controller");
|
||||
const requirementsCtrl = require("../../controllers/admin/completion_requirements.controller");
|
||||
|
||||
// Guards are applied by admin.routes.js (authenticate → requireAdmin → adminLimiter)
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// COURSES
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
router.get("/", ctrl.getCourses);
|
||||
router.post("/", ctrl.createCourse);
|
||||
router.post("/full", ctrl.createCourseFull); // one-shot Add Course wizard submit (course + roadmap + rewards)
|
||||
|
||||
// ── static segments first ────────────────────────────────────────────────────
|
||||
router.get("/field-values", ctrl.getCourseFieldValues);
|
||||
router.delete("/bulk", ctrl.bulkArchiveCourses);
|
||||
router.delete("/bulk/permanent", ctrl.bulkPermanentlyDeleteCourses);
|
||||
router.get("/archives", ctrl.getArchivedCourses);
|
||||
router.patch("/restore/bulk", ctrl.bulkRestoreCourses);
|
||||
router.get("/flat", ctrl.getCoursesFlat);
|
||||
router.get("/by-subscription", ctrl.getCoursesBySubscription);
|
||||
router.get("/units-flat", ctrl.getUnitsFlat);
|
||||
router.get("/lessons-flat", ctrl.getLessonsFlat);
|
||||
router.get("/uuid/:uuid/structure-counts", ctrl.getCourseStructureCounts);
|
||||
router.put("/order", ctrl.reorderCourses); // persist top-level course catalog ordering { course_ids }
|
||||
|
||||
// ── then :courseId ────────────────────────────────────────────────────────────
|
||||
router.get("/archives/:courseId", ctrl.getArchivedCourse);
|
||||
router.patch("/:courseId/restore", ctrl.restoreCourse);
|
||||
router.get("/:courseId/archive-impact", ctrl.getCourseArchiveImpact);
|
||||
router.get("/:courseId/permanent-delete-impact", ctrl.getCoursePermanentDeleteImpact);
|
||||
router.get("/:courseId", ctrl.getCourse);
|
||||
router.put("/:courseId", ctrl.updateCourse);
|
||||
router.delete("/:courseId", ctrl.archiveCourse);
|
||||
router.delete("/:courseId/permanent", ctrl.permanentlyDeleteCourse);
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// COURSE INSTRUCTORS
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
router.get("/:courseId/instructors", ctrl.getInstructors);
|
||||
router.put("/:courseId/instructors", ctrl.syncInstructors);
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// COURSE ACHIEVEMENTS
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
router.get("/:courseId/achievements", ctrl.getCourseAchievements);
|
||||
router.put("/:courseId/achievements", ctrl.syncCourseAchievements);
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// PREREQUISITES
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
router.get("/:courseId/prerequisites", ctrl.getPrerequisites);
|
||||
router.put("/:courseId/prerequisites", ctrl.syncPrerequisites);
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// COMPLETION REQUIREMENTS
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
router.get("/:courseId/requirements", requirementsCtrl.getCourseRequirements);
|
||||
router.put("/:courseId/requirements", requirementsCtrl.syncCourseRequirements);
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// COURSE ASSESSMENT
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
router.get("/:courseId/assessment", ctrl.getAssessment);
|
||||
router.post("/:courseId/assessment", ctrl.createAssessment);
|
||||
|
||||
// ── static segments first ────────────────────────────────────────────────────
|
||||
router.get("/:courseId/assessment/archives", ctrl.getArchivedAssessment);
|
||||
|
||||
// ── then :assessmentId ────────────────────────────────────────────────────────
|
||||
router.patch("/:courseId/assessment/:assessmentId", ctrl.updateAssessment);
|
||||
router.delete("/:courseId/assessment/:assessmentId", ctrl.deleteAssessment);
|
||||
router.patch("/:courseId/assessment/:assessmentId/restore", ctrl.restoreAssessment);
|
||||
|
||||
// ── Assessment Questions ──────────────────────────────────────────────────────
|
||||
router.get("/:courseId/assessment/:assessmentId/questions", ctrl.getQuestions);
|
||||
router.post("/:courseId/assessment/:assessmentId/questions", ctrl.createQuestion);
|
||||
|
||||
// static before :questionId
|
||||
router.delete("/:courseId/assessment/:assessmentId/questions/bulk", ctrl.bulkArchiveQuestions);
|
||||
router.patch("/:courseId/assessment/:assessmentId/questions/restore/bulk", ctrl.bulkRestoreQuestions);
|
||||
router.put("/:courseId/assessment/:assessmentId/questions/bulk-sync", ctrl.bulkSyncQuestions);
|
||||
router.get("/:courseId/assessment/:assessmentId/questions/archives/:questionId", ctrl.getArchivedQuestion);
|
||||
|
||||
router.patch("/:courseId/assessment/:assessmentId/questions/:questionId", ctrl.updateQuestion);
|
||||
router.delete("/:courseId/assessment/:assessmentId/questions/:questionId", ctrl.deleteQuestion);
|
||||
router.patch("/:courseId/assessment/:assessmentId/questions/:questionId/restore", ctrl.restoreQuestion);
|
||||
|
||||
// ── Assessment Completions & Sessions ─────────────────────────────────────────
|
||||
router.get("/:courseId/assessment/:assessmentId/completions", ctrl.getAssessmentCompletions);
|
||||
router.get("/:courseId/assessment/:assessmentId/sessions", ctrl.getAssessmentSessions);
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// UNITS
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
router.get("/:courseId/field-values", ctrl.getUnitFieldValues);
|
||||
router.get("/:courseId/units", ctrl.getUnits);
|
||||
router.post("/:courseId/units", ctrl.createUnit); // create new unit + attach, or attach existing via body { unit_id }
|
||||
|
||||
// static before :unitId
|
||||
router.post("/:courseId/units/attach", ctrl.attachUnits); // bulk-attach existing library units { unit_ids }
|
||||
router.put("/:courseId/units/order", ctrl.reorderUnits); // persist course-level unit ordering { unit_ids }
|
||||
router.delete("/:courseId/units/bulk", ctrl.bulkArchiveUnits);
|
||||
router.delete("/:courseId/units/bulk/permanent", ctrl.bulkPermanentlyDeleteUnits);
|
||||
router.get("/:courseId/units/archives", ctrl.getArchivedUnits);
|
||||
router.patch("/:courseId/units/restore/bulk", ctrl.bulkRestoreUnits);
|
||||
|
||||
router.get("/:courseId/units/archives/:unitId", ctrl.getArchivedUnit);
|
||||
router.patch("/:courseId/units/:unitId/restore", ctrl.restoreUnit);
|
||||
router.get("/:courseId/units/:unitId/archive-impact", ctrl.getUnitArchiveImpact);
|
||||
router.get("/:courseId/units/:unitId/permanent-delete-impact", ctrl.getUnitPermanentDeleteImpact);
|
||||
router.get("/:courseId/units/:unitId", ctrl.getUnit);
|
||||
router.put("/:courseId/units/:unitId", ctrl.updateUnit);
|
||||
router.delete("/:courseId/units/:unitId", ctrl.archiveUnit);
|
||||
router.delete("/:courseId/units/:unitId/permanent", ctrl.permanentlyDeleteUnit);
|
||||
|
||||
router.get("/:courseId/units/:unitId/requirements", requirementsCtrl.getUnitRequirements);
|
||||
router.put("/:courseId/units/:unitId/requirements", requirementsCtrl.syncUnitRequirements);
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// UNIT QUIZ
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
router.get("/:courseId/units/:unitId/quiz", ctrl.getQuiz);
|
||||
router.post("/:courseId/units/:unitId/quiz", ctrl.createQuiz);
|
||||
|
||||
// static before :quizId
|
||||
router.get("/:courseId/units/:unitId/quiz/archives", ctrl.getArchivedQuiz);
|
||||
|
||||
router.patch("/:courseId/units/:unitId/quiz/:quizId", ctrl.updateQuiz);
|
||||
router.delete("/:courseId/units/:unitId/quiz/:quizId", ctrl.deleteQuiz);
|
||||
router.patch("/:courseId/units/:unitId/quiz/:quizId/restore", ctrl.restoreQuiz);
|
||||
|
||||
// ── Quiz Questions ────────────────────────────────────────────────────────────
|
||||
router.get("/:courseId/units/:unitId/quiz/:quizId/questions", ctrl.getQuestions);
|
||||
router.post("/:courseId/units/:unitId/quiz/:quizId/questions", ctrl.createQuestion);
|
||||
|
||||
// static before :questionId
|
||||
router.delete("/:courseId/units/:unitId/quiz/:quizId/questions/bulk", ctrl.bulkArchiveQuestions);
|
||||
router.patch("/:courseId/units/:unitId/quiz/:quizId/questions/restore/bulk", ctrl.bulkRestoreQuestions);
|
||||
router.put("/:courseId/units/:unitId/quiz/:quizId/questions/bulk-sync", ctrl.bulkSyncQuestions);
|
||||
router.get("/:courseId/units/:unitId/quiz/:quizId/questions/archives/:questionId", ctrl.getArchivedQuestion);
|
||||
|
||||
router.patch("/:courseId/units/:unitId/quiz/:quizId/questions/:questionId", ctrl.updateQuestion);
|
||||
router.delete("/:courseId/units/:unitId/quiz/:quizId/questions/:questionId", ctrl.deleteQuestion);
|
||||
router.patch("/:courseId/units/:unitId/quiz/:quizId/questions/:questionId/restore", ctrl.restoreQuestion);
|
||||
|
||||
// ── Quiz Completions ──────────────────────────────────────────────────────────
|
||||
router.get("/:courseId/units/:unitId/quiz/:quizId/completions", ctrl.getQuizCompletions);
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// LESSONS
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
router.get("/:courseId/units/:unitId/field-values", ctrl.getLessonFieldValues);
|
||||
router.get("/:courseId/units/:unitId/lessons", ctrl.getLessons);
|
||||
router.post("/:courseId/units/:unitId/lessons", ctrl.createLesson); // create new lesson + attach, or attach existing via body { lesson_id }
|
||||
|
||||
// static before :lessonId
|
||||
router.put("/:courseId/units/:unitId/lessons/order", ctrl.reorderLessons); // persist unit-level lesson ordering { lesson_ids }
|
||||
router.delete("/:courseId/units/:unitId/lessons/bulk", ctrl.bulkArchiveLessons);
|
||||
router.delete("/:courseId/units/:unitId/lessons/bulk/permanent", ctrl.bulkPermanentlyDeleteLessons);
|
||||
router.get("/:courseId/units/:unitId/lessons/archives", ctrl.getArchivedLessons);
|
||||
router.patch("/:courseId/units/:unitId/lessons/restore/bulk", ctrl.bulkRestoreLessons);
|
||||
|
||||
router.get("/:courseId/units/:unitId/lessons/archives/:lessonId", ctrl.getArchivedLesson);
|
||||
router.patch("/:courseId/units/:unitId/lessons/:lessonId/restore", ctrl.restoreLesson);
|
||||
router.get("/:courseId/units/:unitId/lessons/:lessonId", ctrl.getLesson);
|
||||
router.put("/:courseId/units/:unitId/lessons/:lessonId", ctrl.updateLesson);
|
||||
router.delete("/:courseId/units/:unitId/lessons/:lessonId", ctrl.archiveLesson);
|
||||
router.delete("/:courseId/units/:unitId/lessons/:lessonId/permanent", ctrl.permanentlyDeleteLesson);
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// LESSON PAGE
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
router.get("/:courseId/units/:unitId/lessons/:lessonId/page", ctrl.getLessonPage);
|
||||
router.put("/:courseId/units/:unitId/lessons/:lessonId/page", ctrl.upsertLessonPage);
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// LESSON COMPLETION REQUIREMENTS
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
router.get("/:courseId/units/:unitId/lessons/:lessonId/requirements", requirementsCtrl.getLessonRequirements);
|
||||
router.put("/:courseId/units/:unitId/lessons/:lessonId/requirements", requirementsCtrl.syncLessonRequirements);
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// COURSE READING PROGRESS
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// static segment before :userId to avoid param collision
|
||||
router.get("/:courseId/reading-progress/users/:userId", progressCtrl.getUserReadingProgress);
|
||||
router.get("/:courseId/reading-progress", progressCtrl.getCourseReadingProgress);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,9 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
const { getUsersDashboard, getGroupsDashboard } = require('../../controllers/admin/dashboard.controller');
|
||||
|
||||
router.get('/users', getUsersDashboard);
|
||||
router.get('/groups', getGroupsDashboard);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,25 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
const groupsCtrl = require('../../controllers/admin/user_groups.controller');
|
||||
const { sensitiveOpsLimiter } = require('../../middleware/rateLimiter.middleware');
|
||||
|
||||
// ── Groups ────────────────────────────────────────────────────────────────────
|
||||
router.get('/field-values', groupsCtrl.getGroupFieldValues);
|
||||
router.post('/bulk/restore', sensitiveOpsLimiter, groupsCtrl.bulkRestoreGroups);
|
||||
router.delete('/bulk', sensitiveOpsLimiter, groupsCtrl.bulkDeactivateGroups);
|
||||
router.delete('/bulk/permanent', sensitiveOpsLimiter, groupsCtrl.bulkPermanentlyDeleteGroups);
|
||||
router.get('/archived', groupsCtrl.getArchivedGroups);
|
||||
router.get('/', groupsCtrl.getGroups);
|
||||
router.get('/:gid', groupsCtrl.getGroup);
|
||||
router.post('/', sensitiveOpsLimiter, groupsCtrl.createGroup);
|
||||
router.put('/:gid', sensitiveOpsLimiter, groupsCtrl.updateGroup);
|
||||
router.patch('/:gid/deactivate', sensitiveOpsLimiter, groupsCtrl.deactivateGroup);
|
||||
router.patch('/:gid/restore', sensitiveOpsLimiter, groupsCtrl.restoreGroup);
|
||||
router.delete('/:gid/permanent', sensitiveOpsLimiter, groupsCtrl.permanentlyDeleteGroup);
|
||||
router.get('/:gid/users', groupsCtrl.getUsersInGroup);
|
||||
router.get('/:gid/users/add', groupsCtrl.getUsersNotInGroup);
|
||||
router.post('/:gid/users', sensitiveOpsLimiter, groupsCtrl.addUserToGroup);
|
||||
router.delete('/:gid/users', sensitiveOpsLimiter, groupsCtrl.removeUserFromGroup);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,45 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: lessons.routes.js (admin)
|
||||
* Type of Program: Router
|
||||
* Description: Standalone Lesson library — Lessons run independently of Units.
|
||||
* Unit membership (attach/detach/reorder) lives under
|
||||
* /admin/units/:unitId/lessons.
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jul. 7, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const router = require("express").Router();
|
||||
const ctrl = require("../../controllers/admin/lessons.controller");
|
||||
const requirementsCtrl = require("../../controllers/admin/completion_requirements.controller");
|
||||
|
||||
// Guards are applied by admin.routes.js (authenticate → requireAdmin → adminLimiter)
|
||||
|
||||
// ── static segments first ─────────────────────────────────────────────────────
|
||||
router.get("/", ctrl.getLessons);
|
||||
router.post("/", ctrl.createLesson);
|
||||
router.get("/flat", ctrl.getLessonsFlat);
|
||||
router.get("/by-subscription", ctrl.getLessonsBySubscription);
|
||||
router.get("/field-values", ctrl.getLessonFieldValues);
|
||||
router.delete("/bulk", ctrl.bulkArchiveLessons);
|
||||
router.delete("/bulk/permanent", ctrl.bulkPermanentlyDeleteLessons);
|
||||
router.get("/archives", ctrl.getArchivedLessons);
|
||||
router.patch("/restore/bulk", ctrl.bulkRestoreLessons);
|
||||
router.get("/archives/:lessonId", ctrl.getArchivedLesson);
|
||||
|
||||
// ── then :lessonId ────────────────────────────────────────────────────────────
|
||||
router.patch("/:lessonId/restore", ctrl.restoreLesson);
|
||||
router.get("/:lessonId/permanent-delete-impact", ctrl.getLessonPermanentDeleteImpact);
|
||||
router.get("/:lessonId", ctrl.getLesson);
|
||||
router.put("/:lessonId", ctrl.updateLesson);
|
||||
router.delete("/:lessonId", ctrl.archiveLesson);
|
||||
router.delete("/:lessonId/permanent", ctrl.permanentlyDeleteLesson);
|
||||
|
||||
// ── Lesson page (block content) ───────────────────────────────────────────────
|
||||
router.get("/:lessonId/page", ctrl.getLessonPage);
|
||||
router.put("/:lessonId/page", ctrl.upsertLessonPage);
|
||||
|
||||
// ── Completion requirements (shared with the nested courses.routes.js mount) ──
|
||||
router.get("/:lessonId/requirements", requirementsCtrl.getLessonRequirements);
|
||||
router.put("/:lessonId/requirements", requirementsCtrl.syncLessonRequirements);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,16 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: media.routes.js (admin)
|
||||
* Type of Program: Routes
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jun. 22, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
"use strict";
|
||||
|
||||
const router = require("express").Router();
|
||||
const ctrl = require("../../controllers/admin/media.controller");
|
||||
|
||||
// Auth + requireAdmin are already applied by admin.routes.js before reaching here.
|
||||
router.post("/token", ctrl.issueToken);
|
||||
router.post("/tokens", ctrl.issueTokensBatch);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,24 @@
|
||||
// routes/admin/notificationBroadcasts.routes.js
|
||||
const router = require('express').Router();
|
||||
const controller = require('../../controllers/admin/notificationBroadcasts.controller');
|
||||
const { sensitiveOpsLimiter } = require('../../middleware/rateLimiter.middleware');
|
||||
|
||||
// ─── Static routes first ──────────────────────────────────────────────────────
|
||||
router.get('/archived', controller.getArchivedBroadcasts);
|
||||
router.delete('/bulk', sensitiveOpsLimiter, controller.archiveBroadcasts);
|
||||
router.patch('/bulk-restore', controller.restoreBroadcasts);
|
||||
router.delete('/bulk/permanent', sensitiveOpsLimiter, controller.permanentlyDeleteBroadcasts);
|
||||
|
||||
// ─── Collection ───────────────────────────────────────────────────────────────
|
||||
router.get('/', controller.getBroadcasts);
|
||||
router.post('/', controller.createBroadcast);
|
||||
|
||||
// ─── Dynamic routes last ──────────────────────────────────────────────────────
|
||||
router.get('/:broadcastId', controller.getBroadcast);
|
||||
router.patch('/:broadcastId', sensitiveOpsLimiter, controller.updateBroadcast);
|
||||
router.patch('/:broadcastId/send', sensitiveOpsLimiter, controller.sendBroadcast);
|
||||
router.patch('/:broadcastId/restore', sensitiveOpsLimiter, controller.restoreBroadcast);
|
||||
router.delete('/:broadcastId', sensitiveOpsLimiter, controller.archiveBroadcast);
|
||||
router.delete('/:broadcastId/permanent', sensitiveOpsLimiter, controller.permanentlyDeleteBroadcast);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,8 @@
|
||||
// routes/admin/notificationSettings.routes.js
|
||||
const router = require('express').Router();
|
||||
const controller = require('../../controllers/admin/notificationSettings.controller');
|
||||
|
||||
router.get('/', controller.getSettings);
|
||||
router.patch('/:jobName', controller.updateSetting);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,26 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name : notifications.routes.js
|
||||
* Type : Router (Admin)
|
||||
* Description : Admin notification endpoints.
|
||||
*
|
||||
* GET /api/admin/notifications — paginated list
|
||||
* GET /api/admin/notifications/unseen — unseen count
|
||||
* PATCH /api/admin/notifications/seen-all — mark all seen
|
||||
* PATCH /api/admin/notifications/:id/seen — mark one seen
|
||||
*
|
||||
* Guards: inherited from admin.routes.js (authenticate → requireAdmin → adminLimiter)
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jun. 19, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { list, unseenCount, stickyAnnouncement, markSeen, markAllSeen } = require('../../controllers/admin/notification.controller');
|
||||
|
||||
router.get('/', list);
|
||||
router.get('/unseen', unseenCount);
|
||||
router.get('/sticky', stickyAnnouncement);
|
||||
router.patch('/seen-all', markAllSeen);
|
||||
router.patch('/:id/seen', markSeen);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,14 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const ctrl = require('../../controllers/admin/products.controller');
|
||||
|
||||
// Product per course
|
||||
router.get ('/courses/:courseId/product', ctrl.getCourseProduct);
|
||||
router.put ('/courses/:courseId/product', ctrl.upsertCourseProduct);
|
||||
router.delete('/courses/:courseId/product', ctrl.removeCourseProduct);
|
||||
|
||||
// Categories per course
|
||||
router.get ('/courses/:courseId/categories', ctrl.getCourseCategories);
|
||||
router.post ('/courses/:courseId/categories', ctrl.syncCourseCategories);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,14 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
const ctrl = require('../../controllers/admin/profile.controller');
|
||||
const { validate } = require('../../middleware/validate.middleware');
|
||||
const { updateProfileValidator } = require('../../validators/profile.validator');
|
||||
const { handleAvatarUpload } = require('../../middleware/avatar_upload.middleware');
|
||||
|
||||
router.get('/', ctrl.getProfile);
|
||||
router.put('/', ...updateProfileValidator, validate, ctrl.updateProfile);
|
||||
router.post('/avatar', handleAvatarUpload, ctrl.uploadAvatar);
|
||||
router.delete('/avatar', ctrl.deleteAvatar);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,65 @@
|
||||
const router = require("express").Router();
|
||||
const controller = require("../../controllers/admin/task.controller");
|
||||
const completionController = require("../../controllers/admin/task_completion.controller");
|
||||
const { sensitiveOpsLimiter } = require("../../middleware/rateLimiter.middleware");
|
||||
|
||||
// NOTE on rate limiting: every route below already sits behind adminLimiter
|
||||
// (200 req/15min per admin user — applied blanket-wide in admin.routes.js).
|
||||
// sensitiveOpsLimiter (10 req/hour, ALSO keyed per admin user and shared
|
||||
// across every route it's attached to) is reserved for genuinely irreversible
|
||||
// actions — permanent delete — so routine CRUD doesn't compete with those for
|
||||
// the same tight budget.
|
||||
|
||||
// ─── Task Lists ───────────────────────────────────────────────────────────────
|
||||
router.get("/", controller.getTaskLists);
|
||||
router.get("/archived", controller.getArchivedTaskLists);
|
||||
router.get("/field-values", controller.getTaskListFieldValues);
|
||||
router.post("/", controller.createTaskList);
|
||||
router.post("/bulk-archive", controller.bulkArchiveTaskLists);
|
||||
router.post("/bulk-restore", controller.bulkRestoreTaskLists);
|
||||
router.post("/bulk-delete", sensitiveOpsLimiter, controller.bulkPermanentlyDeleteTaskLists);
|
||||
|
||||
// ─── Task List (single) ───────────────────────────────────────────────────────
|
||||
router.get("/:taskListId", controller.getTaskList);
|
||||
router.get("/:taskListId/permanent-delete-impact", controller.getTaskListPermanentDeleteImpact);
|
||||
router.patch("/:taskListId", controller.updateTaskList);
|
||||
router.delete("/:taskListId", controller.archiveTaskList);
|
||||
router.delete("/:taskListId/permanent", sensitiveOpsLimiter, controller.permanentlyDeleteTaskList);
|
||||
router.patch("/:taskListId/restore", controller.restoreTaskList);
|
||||
|
||||
// ─── Task List Groups ─────────────────────────────────────────────────────────
|
||||
router.get("/:taskListId/groups", controller.getTaskListGroups);
|
||||
router.put("/:taskListId/groups", controller.syncGroups);
|
||||
router.post("/:taskListId/groups/assign", controller.assignGroups);
|
||||
router.post("/:taskListId/groups/unassign", controller.unassignGroups);
|
||||
|
||||
// ─── Tasks (nested under task-list) ──────────────────────────────────────────
|
||||
router.get("/:taskListId/tasks", controller.getTasks);
|
||||
router.get("/:taskListId/tasks/archived", controller.getArchivedTasks);
|
||||
router.get("/:taskListId/tasks/field-values", controller.getTaskFieldValues);
|
||||
router.get("/:taskListId/tasks/requirement-completion-check", controller.checkRequirementCompletion);
|
||||
router.post("/:taskListId/tasks", controller.createTask);
|
||||
router.post("/:taskListId/tasks/bulk", controller.createTasksBulk);
|
||||
router.post("/:taskListId/tasks/bulk-archive", controller.bulkArchiveTasks);
|
||||
router.post("/:taskListId/tasks/bulk-restore", controller.bulkRestoreTasks);
|
||||
router.post("/:taskListId/tasks/bulk-delete", sensitiveOpsLimiter, controller.bulkPermanentlyDeleteTasks);
|
||||
router.patch("/:taskListId/tasks/order", controller.reorderTasks);
|
||||
|
||||
// ─── Tasks (single) ───────────────────────────────────────────────────────────
|
||||
router.get("/:taskListId/tasks/:taskId", controller.getTask);
|
||||
router.patch("/:taskListId/tasks/:taskId", controller.updateTask);
|
||||
router.delete("/:taskListId/tasks/:taskId", controller.archiveTask);
|
||||
router.delete("/:taskListId/tasks/:taskId/permanent", sensitiveOpsLimiter, controller.permanentlyDeleteTask);
|
||||
router.patch("/:taskListId/tasks/:taskId/restore", controller.restoreTask);
|
||||
|
||||
// ─── Completions (nested under task) ─────────────────────────────────────────
|
||||
router.get("/:taskListId/tasks/:taskId/completions", completionController.getCompletions);
|
||||
router.get("/:taskListId/tasks/:taskId/completions/user/:userId", completionController.getCompletionsByUser);
|
||||
router.post("/:taskListId/tasks/:taskId/completions/bulk-archive", completionController.bulkArchiveCompletions);
|
||||
router.post("/:taskListId/tasks/:taskId/completions/bulk-restore", completionController.bulkRestoreCompletions);
|
||||
router.get("/:taskListId/tasks/:taskId/completions/:completionId", completionController.getCompletion);
|
||||
router.patch("/:taskListId/tasks/:taskId/completions/:completionId/review", completionController.reviewSubmission);
|
||||
router.delete("/:taskListId/tasks/:taskId/completions/:completionId", completionController.archiveCompletion);
|
||||
router.patch("/:taskListId/tasks/:taskId/completions/:completionId/restore", completionController.restoreCompletion);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
const router = require('express').Router();
|
||||
const ctrl = require('../../controllers/admin/tier_categories.controller');
|
||||
|
||||
// Auth + requireAdmin applied by admin.routes.js
|
||||
|
||||
router.get ('/', ctrl.getCategories);
|
||||
router.post ('/', ctrl.createCategory);
|
||||
router.get ('/:id', ctrl.getCategory);
|
||||
router.put ('/:id', ctrl.updateCategory);
|
||||
router.delete('/:id', ctrl.deleteCategory);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,19 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const ctrl = require('../../controllers/admin/tier_policies.controller');
|
||||
|
||||
// ─── Payment Policies (promo codes + refund window) ───────────────────────────
|
||||
// GET /admin/tiers/plans/:planId/payment-policy
|
||||
// PUT /admin/tiers/plans/:planId/payment-policy — JSON: { promo_rules, refund_policy, allowed_providers }
|
||||
router.get('/plans/:planId/payment-policy', ctrl.getPaymentPolicy);
|
||||
router.put('/plans/:planId/payment-policy', ctrl.upsertPaymentPolicy);
|
||||
|
||||
// ─── System Badges ────────────────────────────────────────────────────────────
|
||||
// GET /admin/tiers/system-badges
|
||||
// GET /admin/tiers/system-badges/:key
|
||||
// PUT /admin/tiers/system-badges/:key — JSON: { asset_id, label, active_from, active_until, … }
|
||||
router.get('/system-badges', ctrl.getSystemBadges);
|
||||
router.get('/system-badges/:key', ctrl.getSystemBadge);
|
||||
router.put('/system-badges/:key', ctrl.upsertSystemBadge);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,37 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const ctrl = require('../../controllers/admin/tiers.controller');
|
||||
|
||||
router.get ('/currencies', ctrl.getCurrencies);
|
||||
router.get ('/', ctrl.getPlans);
|
||||
router.post ('/', ctrl.createPlan);
|
||||
router.get ('/field-values', ctrl.getPlanFieldValues);
|
||||
router.post ('/bulk/archive', ctrl.bulkArchivePlans);
|
||||
router.post ('/bulk/restore', ctrl.bulkRestorePlans);
|
||||
router.post ('/bulk/permanent-delete', ctrl.bulkPermanentlyDeletePlans);
|
||||
|
||||
router.get ('/payments/field-values', ctrl.getPaymentFieldValues);
|
||||
router.get ('/payments', ctrl.getPayments);
|
||||
router.get ('/payments/:id', ctrl.getPayment);
|
||||
|
||||
router.get ('/users/:id/tiers', ctrl.getUserTiers);
|
||||
router.post ('/users/tiers/grant', ctrl.grantTier);
|
||||
router.patch ('/users/tiers/:tid/revoke', ctrl.revokeTier);
|
||||
|
||||
// ← course/unit/lesson + impact routes before /:id
|
||||
router.get ('/:id/impact', ctrl.getPlanImpact);
|
||||
router.get ('/:id/permanent-delete-impact', ctrl.getPlanPermanentDeleteImpact);
|
||||
router.get ('/:id/courses', ctrl.getPlanCourses);
|
||||
router.post ('/:id/courses', ctrl.syncPlanCourses);
|
||||
router.get ('/:id/units', ctrl.getPlanUnits);
|
||||
router.post ('/:id/units', ctrl.syncPlanUnits);
|
||||
router.get ('/:id/lessons', ctrl.getPlanLessons);
|
||||
router.post ('/:id/lessons', ctrl.syncPlanLessons);
|
||||
|
||||
router.get ('/:id', ctrl.getPlan);
|
||||
router.put ('/:id', ctrl.updatePlan);
|
||||
router.delete('/:id', ctrl.archivePlan);
|
||||
router.post ('/:id/restore', ctrl.restorePlan);
|
||||
router.delete('/:id/permanent', ctrl.permanentlyDeletePlan);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,71 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: units.routes.js (admin)
|
||||
* Type of Program: Router
|
||||
* Description: Standalone Unit library — Units run independently of Courses.
|
||||
* Course membership (attach/detach/reorder) stays under
|
||||
* /admin/courses/:courseId/units.
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jul. 7, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const router = require("express").Router();
|
||||
const ctrl = require("../../controllers/admin/units.controller");
|
||||
const questionCtrl = require("../../controllers/admin/courses.controller"); // shared quiz-question handlers (keyed by quizId)
|
||||
const requirementsCtrl = require("../../controllers/admin/completion_requirements.controller");
|
||||
|
||||
// Guards are applied by admin.routes.js (authenticate → requireAdmin → adminLimiter)
|
||||
|
||||
// ── static segments first ─────────────────────────────────────────────────────
|
||||
router.get("/", ctrl.getUnits);
|
||||
router.post("/", ctrl.createUnit);
|
||||
router.post("/full", ctrl.createUnitFull);
|
||||
router.get("/flat", ctrl.getUnitsFlat);
|
||||
router.get("/by-subscription", ctrl.getUnitsBySubscription);
|
||||
router.get("/field-values", ctrl.getUnitFieldValues);
|
||||
router.delete("/bulk", ctrl.bulkArchiveUnits);
|
||||
router.delete("/bulk/permanent", ctrl.bulkPermanentlyDeleteUnits);
|
||||
router.get("/archives", ctrl.getArchivedUnits);
|
||||
router.patch("/restore/bulk", ctrl.bulkRestoreUnits);
|
||||
router.get("/archives/:unitId", ctrl.getArchivedUnit);
|
||||
|
||||
// ── then :unitId ──────────────────────────────────────────────────────────────
|
||||
router.patch("/:unitId/restore", ctrl.restoreUnit);
|
||||
router.get("/:unitId/archive-impact", ctrl.getUnitArchiveImpact);
|
||||
router.get("/:unitId/permanent-delete-impact", ctrl.getUnitPermanentDeleteImpact);
|
||||
router.get("/:unitId", ctrl.getUnit);
|
||||
router.put("/:unitId", ctrl.updateUnit);
|
||||
router.delete("/:unitId", ctrl.archiveUnit);
|
||||
router.delete("/:unitId/permanent", ctrl.permanentlyDeleteUnit);
|
||||
|
||||
// ── Completion requirements (shared with the nested courses.routes.js mount) ──
|
||||
router.get("/:unitId/requirements", requirementsCtrl.getUnitRequirements);
|
||||
router.put("/:unitId/requirements", requirementsCtrl.syncUnitRequirements);
|
||||
|
||||
// ── Lesson membership (attach / detach / reorder) ─────────────────────────────
|
||||
router.post("/:unitId/lessons", ctrl.attachLessons);
|
||||
router.put("/:unitId/lessons/order", ctrl.reorderLessons);
|
||||
router.delete("/:unitId/lessons/:lessonId", ctrl.detachLesson);
|
||||
|
||||
// ── Unit quiz (travels with the Unit) ─────────────────────────────────────────
|
||||
router.get("/:unitId/quiz", ctrl.getQuiz);
|
||||
router.post("/:unitId/quiz", ctrl.createQuiz);
|
||||
router.get("/:unitId/quiz/archives", ctrl.getArchivedQuiz);
|
||||
router.patch("/:unitId/quiz/:quizId", ctrl.updateQuiz);
|
||||
router.delete("/:unitId/quiz/:quizId", ctrl.deleteQuiz);
|
||||
router.patch("/:unitId/quiz/:quizId/restore", ctrl.restoreQuiz);
|
||||
|
||||
// ── Quiz questions (shared handlers — keyed by quizId only) ───────────────────
|
||||
router.get("/:unitId/quiz/:quizId/questions", questionCtrl.getQuestions);
|
||||
router.post("/:unitId/quiz/:quizId/questions", questionCtrl.createQuestion);
|
||||
router.delete("/:unitId/quiz/:quizId/questions/bulk", questionCtrl.bulkArchiveQuestions);
|
||||
router.patch("/:unitId/quiz/:quizId/questions/restore/bulk", questionCtrl.bulkRestoreQuestions);
|
||||
router.put("/:unitId/quiz/:quizId/questions/bulk-sync", questionCtrl.bulkSyncQuestions);
|
||||
router.get("/:unitId/quiz/:quizId/questions/archives/:questionId", questionCtrl.getArchivedQuestion);
|
||||
router.patch("/:unitId/quiz/:quizId/questions/:questionId", questionCtrl.updateQuestion);
|
||||
router.delete("/:unitId/quiz/:quizId/questions/:questionId", questionCtrl.deleteQuestion);
|
||||
router.patch("/:unitId/quiz/:quizId/questions/:questionId/restore", questionCtrl.restoreQuestion);
|
||||
|
||||
// ── Quiz completions ──────────────────────────────────────────────────────────
|
||||
router.get("/:unitId/quiz/:quizId/completions", questionCtrl.getQuizCompletions);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,40 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
const usersCtrl = require('../../controllers/admin/users.controller');
|
||||
const activityCtrl = require('../../controllers/admin/user_activity.controller');
|
||||
const { sensitiveOpsLimiter } = require('../../middleware/rateLimiter.middleware');
|
||||
|
||||
router.post('/bulk/restore', sensitiveOpsLimiter, usersCtrl.bulkRestoreUsers);
|
||||
router.delete('/bulk', sensitiveOpsLimiter, usersCtrl.bulkDeactivateUsers);
|
||||
router.delete('/bulk/permanent', sensitiveOpsLimiter, usersCtrl.bulkPermanentlyDeleteUsers);
|
||||
router.get('/field-values', usersCtrl.getUserFieldValues);
|
||||
router.get('/archived', usersCtrl.getArchivedUsers);
|
||||
router.post('/staff', sensitiveOpsLimiter, usersCtrl.addStaffUser);
|
||||
router.get('/', usersCtrl.getUsers);
|
||||
router.get('/:id', usersCtrl.getUser);
|
||||
router.put('/:id', sensitiveOpsLimiter, usersCtrl.updateUser);
|
||||
router.delete('/:id', sensitiveOpsLimiter, usersCtrl.deactivateUser);
|
||||
router.delete('/:id/permanent', sensitiveOpsLimiter, usersCtrl.permanentlyDeleteUser);
|
||||
router.post('/:id/restore', sensitiveOpsLimiter, usersCtrl.restoreUser);
|
||||
router.post('/:id/make-admin', sensitiveOpsLimiter, usersCtrl.makeAdmin);
|
||||
router.post('/:id/demote-admin', sensitiveOpsLimiter, usersCtrl.demoteAdmin);
|
||||
|
||||
// ── Bans ──────────────────────────────────────────────────────────────────────
|
||||
router.post('/bulk/ban', sensitiveOpsLimiter, usersCtrl.bulkBanUsers);
|
||||
router.post('/bulk/unban', sensitiveOpsLimiter, usersCtrl.bulkUnbanUsers);
|
||||
router.get('/:id/bans', usersCtrl.getUserBans);
|
||||
router.post('/:id/ban', sensitiveOpsLimiter, usersCtrl.banUser);
|
||||
router.post('/:id/unban', sensitiveOpsLimiter, usersCtrl.unbanUser);
|
||||
|
||||
// ── Sessions ──────────────────────────────────────────────────────────────────
|
||||
router.get('/:id/sessions', usersCtrl.getUserSessions);
|
||||
router.delete('/:id/sessions/:sid', sensitiveOpsLimiter, usersCtrl.terminateSession);
|
||||
|
||||
// ── Achievements ──────────────────────────────────────────────────────────────
|
||||
router.get('/:id/achievements', usersCtrl.getUserAchievements);
|
||||
|
||||
// ── Activity ──────────────────────────────────────────────────────────────────
|
||||
router.get('/:id/activity', activityCtrl.getUserActivity);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,71 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: auth.routes.js
|
||||
* Type of Program: Router
|
||||
* Description: Public authentication routes (no auth required).
|
||||
* CSRF protection is applied to state-mutating endpoints.
|
||||
*
|
||||
* Route Map:
|
||||
* GET /api/auth/csrf-token → get CSRF token (for cookie-based clients)
|
||||
* POST /api/auth/register → system registration (sends OTP)
|
||||
* POST /api/auth/verify-otp → verifies OTP, mints tokens/session — the
|
||||
* single endpoint every auth path funnels
|
||||
* through (registration, login, Google)
|
||||
* POST /api/auth/resend-otp → resend OTP email (only while one is pending)
|
||||
* POST /api/auth/login → validates credentials, sends a login OTP
|
||||
* (no tokens issued here — see verify-otp)
|
||||
* POST /api/auth/refresh → refresh access token
|
||||
* POST /api/auth/logout → logout (requires authenticate)
|
||||
* POST /api/auth/change-password → change password (requires authenticate)
|
||||
* POST /api/auth/forgot-password → same procedure for every acc_type — checks
|
||||
* reg_type is 'system' (not Google), sends OTP
|
||||
* POST /api/auth/verify-reset-otp → checks OTP validity only, does not consume it
|
||||
* or change the password (step 2 of the 3-step
|
||||
* reset flow: email → OTP → new password)
|
||||
* POST /api/auth/reset-password → verifies OTP + sets new password in one step
|
||||
* GET /api/auth/google → initiate Google OIDC (generates state/nonce/PKCE)
|
||||
* GET /api/auth/google/callback → verifies + exchanges code, sends a login OTP,
|
||||
* redirects to the frontend callback page (no query
|
||||
* params — outcome is stashed in a signed cookie)
|
||||
* GET /api/auth/google/result → single-use read of that cookie (otpRequired/email,
|
||||
* or an error), so it never has to live in the URL
|
||||
*
|
||||
* Author: rgrgogu
|
||||
* Date Created: Oct. 6, 2025
|
||||
* Date Modified: Jul. 6, 2026 — Google callback handoff moved from URL params to a signed
|
||||
* cookie + /google/result endpoint (Kenneth Obsequio)
|
||||
***********************************************************************************************************************************************************************/
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
const authCtrl = require('../controllers/auth.controller');
|
||||
const { authenticate } = require('../middleware/auth.middleware');
|
||||
const { authLimiter, otpLimiter, sensitiveOpsLimiter } = require('../middleware/rateLimiter.middleware');
|
||||
const { csrfProtection, getCsrfToken } = require('../middleware/csrf.middleware');
|
||||
const { validate } = require('../middleware/validate.middleware');
|
||||
const {
|
||||
registerValidator, loginValidator,
|
||||
verifyOTPValidator, resendOTPValidator, changePassValidator,
|
||||
forgotPasswordValidator, verifyResetOTPValidator, resetPasswordValidator,
|
||||
} = require('../validators/auth.validator');
|
||||
|
||||
// ── CSRF token (GET — no CSRF needed on GETs) ──────────────────────────────────
|
||||
router.get('/csrf-token', csrfProtection, getCsrfToken);
|
||||
|
||||
// ── System auth ────────────────────────────────────────────────────────────────
|
||||
router.post('/register', ...registerValidator, validate, authCtrl.register);
|
||||
router.post('/verify-otp', otpLimiter, ...verifyOTPValidator, validate, authCtrl.verifyOTP);
|
||||
router.post('/resend-otp', otpLimiter, ...resendOTPValidator, validate, authCtrl.resendOTP);
|
||||
router.post('/login', authLimiter, ...loginValidator, validate, authCtrl.login);
|
||||
router.post('/refresh', authLimiter, authCtrl.refreshToken);
|
||||
router.post('/logout', authenticate, authLimiter, authCtrl.logout);
|
||||
router.post('/change-password', authenticate, sensitiveOpsLimiter, ...changePassValidator, validate, authCtrl.changePassword);
|
||||
router.post('/forgot-password', otpLimiter, ...forgotPasswordValidator, validate, authCtrl.forgotPassword);
|
||||
router.post('/verify-reset-otp', otpLimiter, ...verifyResetOTPValidator, validate, authCtrl.verifyResetOTP);
|
||||
router.post('/reset-password', otpLimiter, ...resetPasswordValidator, validate, authCtrl.resetPassword);
|
||||
|
||||
// ── Google OIDC ────────────────────────────────────────────────────────────────
|
||||
router.get('/google', authLimiter, authCtrl.googleRedirect);
|
||||
router.get('/google/callback', authCtrl.googleCallback);
|
||||
router.get('/google/result', authCtrl.googleResult);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,20 @@
|
||||
// routes/client/advertisements.routes.js
|
||||
const router = require('express').Router();
|
||||
const controller = require('../../controllers/client/advertisements.controller');
|
||||
|
||||
// ─── GET /api/client/advertisements/active?placement=dashboard.hero ───────────
|
||||
router.get('/active', controller.getActiveAdvertisement);
|
||||
|
||||
// ─── GET /api/client/advertisements/active-batch?placements=a,b,c ─────────────
|
||||
router.get('/active-batch', controller.getActiveAdvertisements);
|
||||
|
||||
// ─── GET /api/client/advertisements/active-list?placement=dashboard.hero ──────
|
||||
router.get('/active-list', controller.getActiveAdvertisementList);
|
||||
|
||||
// ─── GET /api/client/advertisements/uuid/:uuid ─────────────────────────────────
|
||||
router.get('/uuid/:uuid', controller.getAdvertisementByUuid);
|
||||
|
||||
// ─── POST /api/client/advertisements/:advertisementId/click ───────────────────
|
||||
router.post('/:advertisementId/click', controller.trackClick);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,72 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: client.routes.js
|
||||
* Type of Program: Router
|
||||
* Description: Protected routes accessible by any authenticated user (client, staff, admin).
|
||||
*
|
||||
* Route Map:
|
||||
* GET /api/client/profile → view own profile
|
||||
* PUT /api/client/profile → update own profile
|
||||
* DELETE /api/client/profile → delete own account (soft-delete)
|
||||
* GET /api/client/sessions → view own active sessions
|
||||
* DELETE /api/client/sessions/:id → revoke own session
|
||||
*
|
||||
* Guards: authenticate → requireClient()
|
||||
* Other routes contain here
|
||||
*
|
||||
* Author: rgrgogu
|
||||
* Date Created: Oct. 6, 2025
|
||||
* Modified: Jun. 13, 2026 (by Kenneth Obsequio (@lash0000)
|
||||
***********************************************************************************************************************************************************************/
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const profileCtrl = require('../../controllers/client/profile.controller');
|
||||
const { authenticate, softAuthenticate } = require('../../middleware/auth.middleware');
|
||||
const { requireClient } = require('../../middleware/rbac.middleware');
|
||||
const { validate } = require('../../middleware/validate.middleware');
|
||||
const { updateProfileValidator } = require('../../validators/profile.validator');
|
||||
const { handleAvatarUpload } = require('../../middleware/avatar_upload.middleware');
|
||||
|
||||
const tiersRoutes = require('./tiers.routes');
|
||||
const coursesRoutes = require('./courses.routes');
|
||||
const unitsRoutes = require('./units.routes');
|
||||
const lessonsRoutes = require('./lessons.routes');
|
||||
const coursePurchasesRoutes = require('./course_purchases.routes');
|
||||
const mediaRoutes = require("./media.routes");
|
||||
const groupsRoutes = require("./groups.routes");
|
||||
const advertisementRoutes = require('./advertisements.routes');
|
||||
const notificationRoutes = require('./notifications.routes');
|
||||
const certificateCtrl = require('../../controllers/client/certificate.controller');
|
||||
|
||||
// ── Media routes FIRST — before global auth ───────────────────────────────────
|
||||
// /stream/:token is authenticated by the signed JWT in the URL param, not by
|
||||
// session cookies. Browser media elements don't send auth headers on range requests.
|
||||
router.use('/media', mediaRoutes);
|
||||
|
||||
// ── Soft-auth routes — return null instead of 401 when unauthenticated ────────
|
||||
const { unseenCount } = require('../../controllers/client/notification.controller');
|
||||
router.get('/notifications/unseen', softAuthenticate, unseenCount);
|
||||
|
||||
// ── Apply auth + role to all remaining routes ─────────────────────────────────
|
||||
router.use(authenticate, requireClient());
|
||||
|
||||
router.get('/profile', profileCtrl.getProfile);
|
||||
router.put('/profile', ...updateProfileValidator, validate, profileCtrl.updateProfile);
|
||||
router.delete('/profile', profileCtrl.deleteAccount);
|
||||
router.post('/profile/avatar', handleAvatarUpload, profileCtrl.uploadAvatar);
|
||||
router.delete('/profile/avatar', profileCtrl.deleteAvatar);
|
||||
router.get('/sessions', profileCtrl.getSessions);
|
||||
router.get('/achievements', profileCtrl.getAchievements);
|
||||
router.delete('/sessions/:id', profileCtrl.revokeSession);
|
||||
|
||||
router.use('/tiers', tiersRoutes);
|
||||
router.use('/courses', coursesRoutes);
|
||||
router.use('/units', unitsRoutes); // standalone Unit consumption (junction revamp)
|
||||
router.use('/lessons', lessonsRoutes); // standalone Lesson consumption (junction revamp)
|
||||
router.use('/course-purchases', coursePurchasesRoutes);
|
||||
router.use('/groups', groupsRoutes);
|
||||
router.use('/advertisements', advertisementRoutes);
|
||||
router.use('/notifications', notificationRoutes);
|
||||
|
||||
router.get('/certificates/:courseUuid', certificateCtrl.getCertificate);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,10 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const ctrl = require('../../controllers/client/course_purchases.controller');
|
||||
|
||||
router.get ('/', ctrl.getMyPurchases);
|
||||
router.post('/order', ctrl.createCourseOrder);
|
||||
router.post('/capture', ctrl.captureCourseOrder);
|
||||
router.post('/cancel', ctrl.cancelCourseOrder);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,55 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const ctrl = require('../../controllers/client/courses.controller');
|
||||
const progressCtrl = require('../../controllers/client/course_reading_progress.controller');
|
||||
|
||||
// Course categories for filter chips — must come before /:courseId
|
||||
router.get('/categories', ctrl.getCategories);
|
||||
|
||||
// Profile learning progress card — must come before /:courseId
|
||||
router.get('/in-progress', progressCtrl.getMyInProgressCourses);
|
||||
|
||||
// Live view of every completed lesson/unit/course — must come before /:courseId
|
||||
router.get('/completed', progressCtrl.getMyCompletedContent);
|
||||
|
||||
// UUID lookups (task requirement blocks — must come before /:courseId)
|
||||
router.get('/uuid/:uuid', ctrl.getCourseByUuid);
|
||||
router.get('/unit/uuid/:uuid/lessons', ctrl.getLessonsByUnitUuid);
|
||||
router.get('/unit/uuid/:uuid/task-context', progressCtrl.getUnitTaskContext);
|
||||
router.get('/unit/uuid/:uuid', ctrl.getUnitByUuid);
|
||||
router.get('/lesson/uuid/:uuid/task-context', progressCtrl.getLessonTaskContext);
|
||||
router.get('/lesson/uuid/:uuid', ctrl.getLessonByUuid);
|
||||
|
||||
// Courses
|
||||
router.get('/', ctrl.getCourses);
|
||||
router.get('/:courseId/checkout-info', ctrl.getCourseCheckoutInfo);
|
||||
router.get('/:courseId', ctrl.getCourse);
|
||||
|
||||
// Unit
|
||||
router.get('/:courseId/units/:unitId', ctrl.getUnit);
|
||||
|
||||
// Lesson
|
||||
router.get('/:courseId/units/:unitId/lessons/:lessonId', ctrl.getLesson);
|
||||
|
||||
// Quiz (no answers)
|
||||
router.get('/:courseId/units/:unitId/quiz', ctrl.getUnitQuiz);
|
||||
|
||||
// Assessment
|
||||
router.get( '/:courseId/assessment', ctrl.getCourseAssessment);
|
||||
router.post('/:courseId/assessment/:assessmentId/start', ctrl.startCourseAssessment);
|
||||
router.get( '/:courseId/assessment/:assessmentId/session', ctrl.getAssessmentSession);
|
||||
router.patch('/:courseId/assessment/:assessmentId/draft', ctrl.saveDraft);
|
||||
|
||||
router.patch('/:courseId/units/:unitId/quiz/:quizId/draft', ctrl.saveQuizDraft);
|
||||
router.post('/:courseId/units/:unitId/quiz/:quizId/submit', ctrl.submitUnitQuiz);
|
||||
router.post('/:courseId/assessment/:assessmentId/submit', ctrl.submitCourseAssessment);
|
||||
|
||||
// Reading progress
|
||||
router.get( '/:courseId/progress/summary', progressCtrl.getCourseProgressSummary);
|
||||
router.get( '/:courseId/progress', progressCtrl.getCourseProgress);
|
||||
router.get( '/:courseId/task-context', progressCtrl.getCourseTaskContext);
|
||||
router.post('/:courseId/units/:unitId/lessons/:lessonId/progress', progressCtrl.upsertLessonProgress);
|
||||
router.post('/:courseId/units/:unitId/lessons/:lessonId/watch-progress', progressCtrl.upsertWatchProgress);
|
||||
router.post('/:courseId/units/:unitId/lessons/:lessonId/mark-complete', progressCtrl.markLessonComplete);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,102 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: groups.routes.js (client)
|
||||
* Type of Program: Router
|
||||
* Description: Client-facing task routes — groups, task lists, tasks, completions,
|
||||
* file uploads, and progress tracking.
|
||||
*
|
||||
* Route Map:
|
||||
* GET /client/groups → my groups
|
||||
* GET /client/groups/:groupId → single group
|
||||
* GET /client/groups/:groupId/task-lists → task lists (?status=ongoing|completed|overdue)
|
||||
* GET /client/groups/:groupId/task-lists/:taskListId → single task list
|
||||
* GET /client/groups/:groupId/task-lists/:taskListId/tasks/:taskId → task + requirements + latest completion
|
||||
* GET /client/groups/:groupId/task-lists/:taskListId/tasks/:taskId/progress → full progress snapshot
|
||||
* GET /client/groups/:groupId/task-lists/:taskListId/tasks/:taskId/completions/latest → latest completion only
|
||||
* GET /client/groups/:groupId/task-lists/:taskListId/tasks/:taskId/completions → full completion history
|
||||
* POST /client/groups/:groupId/task-lists/:taskListId/tasks/:taskId/completions → submit completion
|
||||
* POST /client/groups/:groupId/task-lists/:taskListId/tasks/:taskId/upload → upload file to S3 (returns file metadata)
|
||||
* POST /client/groups/:groupId/task-lists/:taskListId/tasks/:taskId/requirements/:requirementId/visit → UPSERT link visit
|
||||
* POST /client/groups/:groupId/task-lists/:taskListId/tasks/:taskId/requirements/:requirementId/progress → UPSERT lesson progress
|
||||
*
|
||||
* Guards: authenticate + requireClient() applied in client.routes.js
|
||||
*
|
||||
* Author: rgrgogu
|
||||
* Date Created: Jun. 13, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const router = require('express').Router();
|
||||
const taskController = require('../../controllers/client/task.controller');
|
||||
const uploadController = require('../../controllers/client/task_upload.controller');
|
||||
const progressController = require('../../controllers/client/task_progress.controller');
|
||||
const downloadController = require('../../controllers/client/task_download.controller');
|
||||
const { handleUpload } = require('../../middleware/task_upload.middleware');
|
||||
const { sensitiveOpsLimiter } = require('../../middleware/rateLimiter.middleware');
|
||||
|
||||
// ─── Groups ───────────────────────────────────────────────────────────────────
|
||||
router.get('/', taskController.getMyGroups);
|
||||
router.get('/:groupId', taskController.getMyGroup);
|
||||
|
||||
// ─── Task Lists ───────────────────────────────────────────────────────────────
|
||||
router.get('/:groupId/task-lists', taskController.getGroupTaskLists);
|
||||
router.get('/:groupId/task-lists/:taskListId', taskController.getGroupTaskList);
|
||||
|
||||
// ─── Tasks ────────────────────────────────────────────────────────────────────
|
||||
router.get('/:groupId/task-lists/:taskListId/tasks/:taskId', taskController.getTask);
|
||||
|
||||
// ─── Progress snapshot ────────────────────────────────────────────────────────
|
||||
router.get(
|
||||
'/:groupId/task-lists/:taskListId/tasks/:taskId/progress',
|
||||
progressController.getTaskProgress
|
||||
);
|
||||
|
||||
// ─── Completions ──────────────────────────────────────────────────────────────
|
||||
// NOTE: /latest must be before / to avoid Express treating "latest" as an ID
|
||||
router.get(
|
||||
'/:groupId/task-lists/:taskListId/tasks/:taskId/completions/latest',
|
||||
taskController.getLatestCompletion
|
||||
);
|
||||
router.get(
|
||||
'/:groupId/task-lists/:taskListId/tasks/:taskId/completions',
|
||||
taskController.getMySubmissions
|
||||
);
|
||||
router.post(
|
||||
'/:groupId/task-lists/:taskListId/tasks/:taskId/completions',
|
||||
sensitiveOpsLimiter,
|
||||
taskController.submitTask
|
||||
);
|
||||
router.get(
|
||||
'/:groupId/task-lists/:taskListId/tasks/:taskId/completions/:completionId/files/:fileId/stream',
|
||||
downloadController.streamCompletionFile
|
||||
);
|
||||
router.get(
|
||||
'/:groupId/task-lists/:taskListId/tasks/:taskId/completions/:completionId/files/:fileId/download',
|
||||
downloadController.downloadCompletionFile
|
||||
);
|
||||
|
||||
// ─── File upload → S3 ────────────────────────────────────────────────────────
|
||||
// Client uploads one file at a time, gets back file metadata,
|
||||
// then includes all file references in the completion submit payload.
|
||||
router.post(
|
||||
'/:groupId/task-lists/:taskListId/tasks/:taskId/upload',
|
||||
sensitiveOpsLimiter,
|
||||
handleUpload,
|
||||
uploadController.uploadTaskFile
|
||||
);
|
||||
|
||||
// ─── Requirement-level progress (UPSERT) ─────────────────────────────────────
|
||||
router.post(
|
||||
'/:groupId/task-lists/:taskListId/tasks/:taskId/requirements/:requirementId/visit',
|
||||
sensitiveOpsLimiter,
|
||||
progressController.visitLink
|
||||
);
|
||||
router.delete(
|
||||
'/:groupId/task-lists/:taskListId/tasks/:taskId/requirements/:requirementId/visit',
|
||||
sensitiveOpsLimiter,
|
||||
progressController.unvisitLink
|
||||
);
|
||||
router.post(
|
||||
'/:groupId/task-lists/:taskListId/tasks/:taskId/requirements/:requirementId/progress',
|
||||
sensitiveOpsLimiter,
|
||||
progressController.updateProgress
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,24 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: lessons.routes.js (client)
|
||||
* Type of Program: Router
|
||||
* Description: Standalone Lesson consumption — each Lesson runs independently.
|
||||
*
|
||||
* GET /client/lessons → learner-facing lesson library
|
||||
* GET /client/lessons/:uuid → lesson with full block content
|
||||
* POST /client/lessons/:uuid/progress → standalone reading progress
|
||||
* (course NULL; body.unit_uuid optional)
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jul. 7, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const ctrl = require('../../controllers/client/units.controller');
|
||||
|
||||
router.get('/', ctrl.getLessons);
|
||||
router.get('/:uuid', ctrl.getLessonByUuid);
|
||||
router.post('/:uuid/progress', ctrl.upsertStandaloneLessonProgress);
|
||||
router.post('/:uuid/watch-progress', ctrl.upsertStandaloneWatchProgress);
|
||||
router.post('/:uuid/mark-complete', ctrl.markStandaloneLessonComplete);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,25 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: media.routes.js (client)
|
||||
* Type of Program: Routes
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jun. 12, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
"use strict";
|
||||
|
||||
const router = require("express").Router();
|
||||
const ctrl = require("../../controllers/client/media.controller");
|
||||
const mediaGuard = require("../../middleware/mediaGuard.middleware");
|
||||
const { authenticate } = require("../../middleware/auth.middleware");
|
||||
|
||||
// ── Stream endpoint — NO mediaGuard, NO authenticate ─────────────────────────
|
||||
// The signed JWT in :token IS the only credential needed.
|
||||
// Browser media/document elements do NOT send Origin/Referer on direct requests
|
||||
// so mediaGuard would always block them.
|
||||
// Supports: video, audio, document (PDF), image
|
||||
router.get("/stream/:token", ctrl.streamAsset);
|
||||
|
||||
// ── All other routes — guarded ────────────────────────────────────────────────
|
||||
router.use(mediaGuard);
|
||||
router.post("/token", authenticate, ctrl.issueToken);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,28 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name : notifications.routes.js
|
||||
* Type : Router (Client)
|
||||
* Description : Per-user notification endpoints.
|
||||
*
|
||||
* GET /api/client/notifications — paginated list
|
||||
* GET /api/client/notifications/unseen — unseen count
|
||||
* PATCH /api/client/notifications/seen-all — mark all seen
|
||||
* PATCH /api/client/notifications/:id/seen — mark one seen
|
||||
* DELETE /api/client/notifications/clear-all — delete all notifications
|
||||
*
|
||||
* Guards: inherited from client.routes.js (authenticate → requireClient)
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jun. 19, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { list, unseenCount, stickyAnnouncement, markSeen, markAllSeen, clearAll } = require('../../controllers/client/notification.controller');
|
||||
|
||||
router.get('/', list);
|
||||
router.get('/unseen', unseenCount);
|
||||
router.get('/sticky', stickyAnnouncement);
|
||||
router.patch('/seen-all', markAllSeen);
|
||||
router.patch('/:id/seen', markSeen);
|
||||
router.delete('/clear-all', clearAll);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,30 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const ctrl = require('../../controllers/client/tiers.controller');
|
||||
|
||||
// My tier
|
||||
router.get ('/me', ctrl.getMyTier);
|
||||
router.get ('/me/history', ctrl.getMyTierHistory);
|
||||
|
||||
// Plans
|
||||
router.get ('/plans', ctrl.getPlans);
|
||||
|
||||
// Promo code validation
|
||||
router.post('/promos/validate', ctrl.validatePromo);
|
||||
|
||||
// Checkout
|
||||
router.post('/checkout/order', ctrl.createOrder);
|
||||
router.post('/checkout/capture', ctrl.captureOrder);
|
||||
router.post('/checkout/cancel', ctrl.cancelOrder);
|
||||
router.post('/checkout/refund', ctrl.refundOrder);
|
||||
|
||||
// My payments
|
||||
router.get ('/me/payments', ctrl.getMyPayments);
|
||||
|
||||
// Tier categories (public — for course badge display)
|
||||
router.get ('/categories', ctrl.getCategories);
|
||||
|
||||
// System badges (public read for profile display)
|
||||
router.get ('/system-badges', ctrl.getSystemBadges);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,26 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: units.routes.js (client)
|
||||
* Type of Program: Router
|
||||
* Description: Standalone Unit consumption — Units run independently of Courses.
|
||||
*
|
||||
* GET /client/units → learner-facing unit library
|
||||
* GET /client/units/:uuid → unit metadata
|
||||
* GET /client/units/:uuid/lessons → unit + ALL lesson data
|
||||
* GET /client/units/:uuid/quiz → quiz (answers stripped)
|
||||
* POST /client/units/:uuid/quiz/:quizId/submit → graded attempt (no course context)
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jul. 7, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const ctrl = require('../../controllers/client/units.controller');
|
||||
|
||||
router.get('/', ctrl.getUnits);
|
||||
router.get('/:uuid/lessons', ctrl.getLessonsByUnitUuid);
|
||||
router.get('/:uuid/quiz', ctrl.getUnitQuiz);
|
||||
router.patch('/:uuid/quiz/:quizId/draft', ctrl.saveUnitQuizDraft);
|
||||
router.post('/:uuid/quiz/:quizId/submit', ctrl.submitUnitQuiz);
|
||||
router.get('/:uuid', ctrl.getUnitByUuid);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,21 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: health.routes.js
|
||||
* Type of Program: Router
|
||||
* Description: Public health check endpoints — no auth, no originGuard.
|
||||
* Intentionally kept outside all guards so monitoring tools can reach them.
|
||||
*
|
||||
* GET /api/health → dashboard (rich: app info, system info, all service connections)
|
||||
* GET /api/health/ready → readiness (compact: status + checks + memory, for machines)
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jun. 20, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
"use strict";
|
||||
|
||||
const router = require('express').Router();
|
||||
const ctrl = require('../controllers/health.controller');
|
||||
|
||||
router.get('/', ctrl.dashboard);
|
||||
router.get('/ready', ctrl.readiness);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,28 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: media.routes.js (public)
|
||||
* Type of Program: Routes
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jun. 22, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
"use strict";
|
||||
|
||||
const rateLimit = require("express-rate-limit");
|
||||
const router = require("express").Router();
|
||||
const ctrl = require("../../controllers/public/media.controller");
|
||||
|
||||
// 60 token requests per IP per 15 min — generous enough for a page with many assets
|
||||
// but still blocks scrapers from bulk-fetching tokens
|
||||
const publicTokenLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 60,
|
||||
keyGenerator: (req) => req.ip,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
message: { status: "error", message: "Too many requests. Please try again later." },
|
||||
});
|
||||
|
||||
// Supports both GET (query string) and POST (body) for flexibility
|
||||
router.get("/token", publicTokenLimiter, ctrl.issueToken);
|
||||
router.post("/token", publicTokenLimiter, ctrl.issueToken);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,21 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: public.routes.js
|
||||
* Type of Program: Router
|
||||
* Description: Unauthenticated public endpoints. originGuard is still applied
|
||||
* (same as other route groups) so requests must originate from the
|
||||
* configured FRONTEND_URL.
|
||||
*
|
||||
* Route Map:
|
||||
* /api/public/media/token → issue stream token for public S3 assets
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jun. 22, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
"use strict";
|
||||
|
||||
const router = require("express").Router();
|
||||
const mediaRoutes = require("./media.routes");
|
||||
|
||||
router.use("/media", mediaRoutes);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,33 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: staff.routes.js
|
||||
* Type of Program: Router
|
||||
* Description: Protected routes accessible by staff and admin only.
|
||||
* Staff can view non-admin users and toggle their status.
|
||||
* Staff cannot view or modify admins.
|
||||
*
|
||||
* Route Map:
|
||||
* GET /api/staff/users → paginated user list (non-admins)
|
||||
* GET /api/staff/users/:id → view a specific non-admin user
|
||||
* PUT /api/staff/users/:id/status → activate or deactivate a user
|
||||
* GET /api/staff/users/:id/sessions → view user's sessions
|
||||
*
|
||||
* Guards: authenticate → requireStaff()
|
||||
*
|
||||
* Author: rgrgogu
|
||||
* Date Created: Oct. 6, 2025
|
||||
***********************************************************************************************************************************************************************/
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
const usersCtrl = require('../../controllers/staff/users.controller');
|
||||
const { authenticate } = require('../../middleware/auth.middleware');
|
||||
const { requireStaff } = require('../../middleware/rbac.middleware');
|
||||
|
||||
router.use(authenticate, requireStaff());
|
||||
|
||||
router.get('/users/', usersCtrl.getUsers);
|
||||
router.get('/users/:id', usersCtrl.getUser);
|
||||
router.put('/users/:id/status', usersCtrl.setUserStatus);
|
||||
router.get('/users/:id/sessions', usersCtrl.getUserSessions);
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user