chore: relocate backend into apps/api ahead of monorepo merge

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 12:20:40 +08:00
co-authored by Claude Sonnet 5
parent af44598df6
commit eaa6d2276a
388 changed files with 0 additions and 13998 deletions
@@ -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;
+79
View File
@@ -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;
+36
View File
@@ -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;
+203
View File
@@ -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;
+25
View File
@@ -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;
+45
View File
@@ -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;
+16
View File
@@ -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;
+14
View File
@@ -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;
+14
View File
@@ -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;
+65
View File
@@ -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;
+37
View File
@@ -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;
+71
View File
@@ -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;
+40
View File
@@ -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;
+71
View File
@@ -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;
+72
View File
@@ -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;
+55
View File
@@ -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;
+102
View File
@@ -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;
+24
View File
@@ -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;
+25
View File
@@ -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;
+30
View File
@@ -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;
+26
View File
@@ -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;
+21
View File
@@ -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;
+28
View File
@@ -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;
+21
View File
@@ -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;
+33
View File
@@ -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;