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,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;