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;