ready to test

Testing

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-06-22 10:06:58 +08:00
parent bf48c95467
commit 439bb33f77
189 changed files with 17559 additions and 686 deletions
+25 -7
View File
@@ -23,22 +23,40 @@ const { authenticate } = require('../../middleware/auth.middleware');
const { requireAdmin } = require('../../middleware/rbac.middleware');
const { adminLimiter } = require('../../middleware/rateLimiter.middleware');
const dashboardRoutes = require('./dashboard.routes');
const usersRoutes = require('./users.routes');
const groupsRoutes = require('./groups.routes');
const assetsRoutes = require('./assets.routes');
const coursesRoutes = require('./courses.routes')
const taskRoutes = require('./task.routes');
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 taskRoutes = require('./task.routes');
const tiersRoutes = require('./tiers.routes');
const categoriesRoutes = require('./categories.routes');
const productsRoutes = require('./products.routes');
const advertisementRoutes = require('./advertisements.routes');
const notificationRoutes = require('./notifications.routes');
const mediaRoutes = require('./media.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('/users', usersRoutes);
router.use('/profile', profileRoutes);
router.use('/users', usersRoutes);
router.use('/groups', groupsRoutes);
router.use('/assets', assetsRoutes);
router.use('/courses', coursesRoutes);
router.use('/task-lists', taskRoutes);
router.use('/tiers', tiersRoutes);
router.use('/categories', categoriesRoutes);
router.use('/products', productsRoutes);
router.use('/advertisements', advertisementRoutes);
router.use('/notifications', notificationRoutes);
router.use('/media', mediaRoutes);
// ── Activity feed — global ────────────────────────────────────────────────────
router.get('/activity', activityCtrl.getActivity);
module.exports = router;
+22
View File
@@ -0,0 +1,22 @@
// 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);
// ─── 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/restore', sensitiveOpsLimiter, controller.restoreAdvertisement);
router.delete('/:advertisementId', sensitiveOpsLimiter, controller.archiveAdvertisement);
module.exports = router;
+9 -9
View File
@@ -1,25 +1,25 @@
const express = require('express');
// routes/admin/assets.routes.js
const router = require('express').Router();
const multer = require('multer');
const upload = multer({ storage: multer.memoryStorage() });
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); // ← must be before /:assetId
router.delete('/bulk', sensitiveOpsLimiter, controller.archiveAssets);
router.get('/archived', controller.getArchivedAssets);
router.get('/field-values', controller.getAssetFieldValues);
router.delete('/bulk', sensitiveOpsLimiter, controller.archiveAssets);
router.patch('/bulk-restore', controller.restoreAssets);
// ─── Collection ───────────────────────────────────────────────────────────────
router.get('/', controller.getAssets);
router.get('/', controller.getAssets);
router.post('/', upload.fields([{ name: 'file', maxCount: 1 }, { name: 'thumbnail', maxCount: 1 }]), controller.uploadAsset);
// ─── Dynamic routes last ──────────────────────────────────────────────────────
router.get('/:assetId', controller.getAsset);
router.patch('/:assetId', sensitiveOpsLimiter, upload.fields([{ name: 'file', maxCount: 1 }]), controller.updateAsset);
router.get('/:assetId', controller.getAsset);
router.patch('/:assetId', sensitiveOpsLimiter, upload.fields([{ name: 'file', maxCount: 1 }]), controller.updateAsset);
router.patch('/:assetId/restore', sensitiveOpsLimiter, controller.restoreAsset);
router.delete('/:assetId', sensitiveOpsLimiter, controller.archiveAsset);
router.delete('/:assetId', sensitiveOpsLimiter, controller.archiveAsset);
module.exports = router;
+12
View File
@@ -0,0 +1,12 @@
const express = require('express');
const router = express.Router();
const ctrl = require('../../controllers/admin/categories.controller');
router.get ('/', ctrl.getCategories);
router.post ('/', ctrl.createCategory);
router.get ('/:id', ctrl.getCategory);
router.put ('/:id', ctrl.updateCategory);
router.delete('/:id', ctrl.archiveCategory);
router.post ('/:id/restore', ctrl.restoreCategory);
module.exports = router;
+24 -5
View File
@@ -1,5 +1,6 @@
const router = require("express").Router();
const ctrl = require("../../controllers/admin/courses.controller");
const ctrl = require("../../controllers/admin/courses.controller");
const progressCtrl = require("../../controllers/admin/course_reading_progress.controller");
const { authenticate } = require("../../middleware/auth.middleware");
const { requireAdmin } = require("../../middleware/rbac.middleware");
@@ -19,6 +20,9 @@ router.get("/field-values", ctrl.getCourseFieldValues);
router.delete("/bulk", ctrl.bulkArchiveCourses);
router.get("/archives", ctrl.getArchivedCourses);
router.patch("/restore/bulk", ctrl.bulkRestoreCourses);
router.get("/flat", ctrl.getCoursesFlat);
router.get("/units-flat", ctrl.getUnitsFlat);
router.get("/lessons-flat", ctrl.getLessonsFlat);
// ── then :courseId ────────────────────────────────────────────────────────────
router.get("/archives/:courseId", ctrl.getArchivedCourse);
@@ -27,6 +31,13 @@ router.get("/:courseId", ctrl.getCourse);
router.put("/:courseId", ctrl.updateCourse);
router.delete("/:courseId", ctrl.archiveCourse);
// ══════════════════════════════════════════════════════════════════════════════
// COURSE INSTRUCTORS
// ══════════════════════════════════════════════════════════════════════════════
router.get("/:courseId/instructors", ctrl.getInstructors);
router.put("/:courseId/instructors", ctrl.syncInstructors);
// ══════════════════════════════════════════════════════════════════════════════
// PREREQUISITES
// ══════════════════════════════════════════════════════════════════════════════
@@ -45,7 +56,7 @@ router.post("/:courseId/assessment", ctrl.createAssessment);
router.get("/:courseId/assessment/archives", ctrl.getArchivedAssessment);
// ── then :assessmentId ────────────────────────────────────────────────────────
router.put("/:courseId/assessment/:assessmentId", ctrl.updateAssessment);
router.patch("/:courseId/assessment/:assessmentId", ctrl.updateAssessment);
router.delete("/:courseId/assessment/:assessmentId", ctrl.deleteAssessment);
router.patch("/:courseId/assessment/:assessmentId/restore", ctrl.restoreAssessment);
@@ -58,7 +69,7 @@ router.delete("/:courseId/assessment/:assessmentId/questions/bulk", ctrl.bulkArc
router.patch("/:courseId/assessment/:assessmentId/questions/restore/bulk", ctrl.bulkRestoreQuestions);
router.get("/:courseId/assessment/:assessmentId/questions/archives/:questionId", ctrl.getArchivedQuestion);
router.put("/:courseId/assessment/:assessmentId/questions/:questionId", ctrl.updateQuestion);
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);
@@ -91,7 +102,7 @@ router.post("/:courseId/units/:unitId/quiz", ctrl.createQuiz);
// static before :quizId
router.get("/:courseId/units/:unitId/quiz/archives", ctrl.getArchivedQuiz);
router.put("/:courseId/units/:unitId/quiz/:quizId", ctrl.updateQuiz);
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);
@@ -104,7 +115,7 @@ router.delete("/:courseId/units/:unitId/quiz/:quizId/questions/bulk", ctrl.bulkA
router.patch("/:courseId/units/:unitId/quiz/:quizId/questions/restore/bulk", ctrl.bulkRestoreQuestions);
router.get("/:courseId/units/:unitId/quiz/:quizId/questions/archives/:questionId", ctrl.getArchivedQuestion);
router.put("/:courseId/units/:unitId/quiz/:quizId/questions/:questionId", ctrl.updateQuestion);
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);
@@ -134,4 +145,12 @@ router.delete("/:courseId/units/:unitId/lessons/:lessonId", ctrl.archiveLesson);
router.get("/:courseId/units/:unitId/lessons/:lessonId/page", ctrl.getLessonPage);
router.put("/:courseId/units/:unitId/lessons/:lessonId/page", ctrl.upsertLessonPage);
// ══════════════════════════════════════════════════════════════════════════════
// 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;
+15
View File
@@ -0,0 +1,15 @@
/***********************************************************************************************************************************************************************
* 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);
module.exports = router;
+25
View File
@@ -0,0 +1,25 @@
/***********************************************************************************************************************************************************************
* 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, markSeen, markAllSeen } = require('../../controllers/admin/notification.controller');
router.get('/', list);
router.get('/unseen', unseenCount);
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;
+10
View File
@@ -1,5 +1,6 @@
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");
// ─── Task Lists ───────────────────────────────────────────────────────────────
@@ -35,4 +36,13 @@ router.patch("/:taskListId/tasks/:taskId", sensitiveOpsLimiter, controller.updat
router.delete("/:taskListId/tasks/:taskId", sensitiveOpsLimiter, controller.archiveTask);
router.patch("/:taskListId/tasks/:taskId/restore", sensitiveOpsLimiter, 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", sensitiveOpsLimiter, completionController.bulkArchiveCompletions);
router.post("/:taskListId/tasks/:taskId/completions/bulk-restore", sensitiveOpsLimiter, completionController.bulkRestoreCompletions);
router.get("/:taskListId/tasks/:taskId/completions/:completionId", completionController.getCompletion);
router.delete("/:taskListId/tasks/:taskId/completions/:completionId", sensitiveOpsLimiter, completionController.archiveCompletion);
router.patch("/:taskListId/tasks/:taskId/completions/:completionId/restore", sensitiveOpsLimiter, completionController.restoreCompletion);
module.exports = router;
+28
View File
@@ -0,0 +1,28 @@
const express = require('express');
const router = express.Router();
const ctrl = require('../../controllers/admin/tiers.controller');
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.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 routes before /:id
router.get ('/:id/courses', ctrl.getPlanCourses);
router.post ('/:id/courses', ctrl.syncPlanCourses);
router.get ('/:id', ctrl.getPlan);
router.put ('/:id', ctrl.updatePlan);
router.delete('/:id', ctrl.archivePlan);
router.post ('/:id/restore', ctrl.restorePlan);
module.exports = router;
+8 -1
View File
@@ -1,7 +1,8 @@
const express = require('express');
const router = express.Router();
const usersCtrl = require('../../controllers/admin/users.controller');
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);
@@ -19,4 +20,10 @@ router.post('/:id/restore', sensitiveOpsLimiter, usersCtrl.restoreUser);
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;
+6 -16
View File
@@ -13,15 +13,14 @@
* POST /api/auth/refresh → refresh access token
* POST /api/auth/logout → logout (requires authenticate)
* POST /api/auth/change-password → change password (requires authenticate)
* GET /api/auth/google → initiate Google OAuth
* GET /api/auth/google/callback → Google OAuth callback
* GET /api/auth/google → initiate Google OIDC (generates state/nonce/PKCE)
* GET /api/auth/google/callback → Google OIDC callback (verifies + exchanges code)
*
* Author: rgrgogu
* Date Created: Oct. 6, 2025
***********************************************************************************************************************************************************************/
const express = require('express');
const passport = require('passport');
const router = express.Router();
const router = express.Router();
const authCtrl = require('../controllers/auth.controller');
const { authenticate } = require('../middleware/auth.middleware');
@@ -45,17 +44,8 @@ router.post('/refresh', authCtrl.refreshToken);
router.post('/logout', authenticate, authLimiter, authCtrl.logout);
router.post('/change-password', authenticate, sensitiveOpsLimiter, ...changePassValidator, validate, authCtrl.changePassword);
// ── Google OAuth ───────────────────────────────────────────────────────────────
router.get('/google',
authLimiter,
passport.authenticate('google', { scope: ['profile', 'email'], session: false })
);
router.get('/google/callback',
passport.authenticate('google', { session: false, failureRedirect: '/api/auth/google/failed' }),
authCtrl.googleCallback
);
router.get('/google/failed', (req, res) => {
res.status(401).json({ status: 'error', message: 'Google authentication failed.' });
});
// ── Google OIDC ────────────────────────────────────────────────────────────────
router.get('/google', authLimiter, authCtrl.googleRedirect);
router.get('/google/callback', authCtrl.googleCallback);
module.exports = router;
+11
View File
@@ -0,0 +1,11 @@
// routes/client/advertisements.routes.js
const router = require('express').Router();
const controller = require('../../controllers/client/advertisements.controller');
// ─── GET /api/client/advertisements/active?type=hero ──────────────────────────
router.get('/active', controller.getActiveAdvertisement);
// ─── POST /api/client/advertisements/:advertisementId/click ───────────────────
router.post('/:advertisementId/click', controller.trackClick);
module.exports = router;
+41 -9
View File
@@ -10,25 +10,57 @@
* 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 express = require('express');
const router = express.Router();
const profileCtrl = require('../../controllers/client/profile.controller');
const { authenticate } = require('../../middleware/auth.middleware');
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');
// Apply auth + role to all routes in this file
const tiersRoutes = require('./tiers.routes');
const coursesRoutes = require('./courses.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.get('/sessions', profileCtrl.getSessions);
router.delete('/sessions/:id', profileCtrl.revokeSession);
router.get('/profile', profileCtrl.getProfile);
router.put('/profile', ...updateProfileValidator, validate, profileCtrl.updateProfile);
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('/course-purchases', coursePurchasesRoutes);
router.use('/groups', groupsRoutes);
router.use('/advertisements', advertisementRoutes);
router.use('/notifications', notificationRoutes);
router.get('/certificates/:courseUuid', certificateCtrl.getCertificate);
module.exports = router;
+10
View File
@@ -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;
+39
View File
@@ -0,0 +1,39 @@
const express = require('express');
const router = express.Router();
const ctrl = require('../../controllers/client/courses.controller');
const progressCtrl = require('../../controllers/client/course_reading_progress.controller');
// Profile learning progress card — must come before /:courseId
router.get('/in-progress', progressCtrl.getMyInProgressCourses);
// 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', ctrl.getUnitByUuid);
router.get('/lesson/uuid/:uuid', ctrl.getLessonByUuid);
// Courses
router.get('/', ctrl.getCourses);
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 (no answers)
router.get('/:courseId/assessment', ctrl.getCourseAssessment);
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.post('/:courseId/units/:unitId/lessons/:lessonId/progress', progressCtrl.upsertLessonProgress);
module.exports = router;
+97
View File
@@ -0,0 +1,97 @@
/***********************************************************************************************************************************************************************
* 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|done|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.post(
'/:groupId/task-lists/:taskListId/tasks/:taskId/requirements/:requirementId/progress',
sensitiveOpsLimiter,
progressController.updateProgress
);
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;
+25
View File
@@ -0,0 +1,25 @@
/***********************************************************************************************************************************************************************
* 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
*
* 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, markSeen, markAllSeen } = require('../../controllers/client/notification.controller');
router.get('/', list);
router.get('/unseen', unseenCount);
router.patch('/seen-all', markAllSeen);
router.patch('/:id/seen', markSeen);
module.exports = router;
+23
View File
@@ -0,0 +1,23 @@
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);
// PayPal checkout
router.post('/checkout/order', ctrl.createOrder);
router.post('/checkout/capture', ctrl.captureOrder);
router.post('/checkout/cancel', ctrl.cancelOrder);
// PayPal refund
router.post('/checkout/refund', ctrl.refundOrder);
// My payments
router.get ('/me/payments', ctrl.getMyPayments);
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;