token issues more for pdf preview missing
With DEV to QAS live test environment Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -12,7 +12,7 @@ const { syncJunction } = require("../../utils/courses/junction.util");
|
||||
const { archiveOne, archiveMany } = require("../../utils/courses/archive.util");
|
||||
const { restoreOne, restoreMany } = require("../../utils/courses/restore.util");
|
||||
const { permanentDeleteOne, permanentDeleteMany } = require("../../utils/courses/permanentDelete.util");
|
||||
const { flattenUnits, flattenLessons, nextOrderIndex, reorderJunction } = require("../../utils/courses/hierarchy.util");
|
||||
const { flattenUnits, flattenLessons, nextOrderIndex, reorderJunction, getCourseUnitIds, countCourseLessons } = require("../../utils/courses/hierarchy.util");
|
||||
const { getFieldValues } = require("../../utils/fieldValues.util");
|
||||
const logActivity = require('../../utils/logActivity.util');
|
||||
const UserNotification = require('../../models/notifications/user_notification.mdl');
|
||||
@@ -2393,6 +2393,36 @@ exports.getCoursesFlat = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// One course's structure counts, looked up by uuid — used by the Task
|
||||
// requirement viewer (ViewTask.jsx) to show how many units/lessons/quizzes a
|
||||
// "Read a Course" requirement actually points to, without pulling the full
|
||||
// nested course tree.
|
||||
exports.getCourseStructureCounts = async (req, res) => {
|
||||
try {
|
||||
const { uuid } = req.params;
|
||||
|
||||
const course = await Course.findOne({ where: { uuid, ...notDeleted }, attributes: ["course_id"] });
|
||||
if (!course) return R.error(res, "Course not found.", 404);
|
||||
|
||||
const unitIds = await getCourseUnitIds(course.course_id);
|
||||
const [lessonCount, quizCount] = await Promise.all([
|
||||
countCourseLessons(course.course_id),
|
||||
unitIds.length
|
||||
? UnitQuiz.count({ where: { unit_id: { [Op.in]: unitIds } } })
|
||||
: 0,
|
||||
]);
|
||||
|
||||
return R.success(res, "Course structure counts retrieved.", {
|
||||
unitCount: unitIds.length,
|
||||
lessonCount,
|
||||
quizCount,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("[COURSE][STRUCTURE COUNTS]", err);
|
||||
return R.error(res, "Could not retrieve course structure counts.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.getCoursesBySubscription = async (req, res) => {
|
||||
try {
|
||||
const { slug } = req.query;
|
||||
|
||||
@@ -558,8 +558,8 @@ exports.unassignGroups = async (req, res) => {
|
||||
// Body: { group_ids: [1, 2, 3] }
|
||||
//
|
||||
// Replaces the full assigned-group set in one request/one transaction, so an
|
||||
// edit that both adds and removes groups only costs a single sensitiveOpsLimiter
|
||||
// hit instead of two (assign + unassign).
|
||||
// edit that both adds and removes groups is a single atomic operation instead
|
||||
// of two separate calls (assign + unassign).
|
||||
|
||||
exports.syncGroups = async (req, res) => {
|
||||
const t = await sequelize.transaction();
|
||||
@@ -1056,6 +1056,102 @@ exports.createTask = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ─── BULK CREATE ──────────────────────────────────────────────────────────────
|
||||
// POST /:taskListId/tasks/bulk
|
||||
// Body: { tasks: [{ name, description, deadline, requirements }, ...] }
|
||||
//
|
||||
// Batches Task + TaskRequirement creation into one transaction/request — used
|
||||
// by the Create Task List wizard so queuing N tasks costs one request instead
|
||||
// of N sequential POST .../tasks calls (see CreateTaskList.jsx handleCreate).
|
||||
// Prerequisite wiring is intentionally not supported here: queued tasks can't
|
||||
// reference a not-yet-created sibling task's id, same as createTask today
|
||||
// when called from this flow.
|
||||
exports.createTasksBulk = async (req, res) => {
|
||||
const t = await sequelize.transaction();
|
||||
try {
|
||||
const { taskListId } = req.params;
|
||||
const { tasks } = req.body;
|
||||
|
||||
if (!Array.isArray(tasks) || !tasks.length)
|
||||
return R.error(res, 'No tasks provided.', 400);
|
||||
if (tasks.some((task) => !task.name)) {
|
||||
return R.error(res, 'Every task requires a name.', 400);
|
||||
}
|
||||
|
||||
const taskList = await TaskList.findByPk(taskListId, { transaction: t });
|
||||
if (!taskList) {
|
||||
await t.rollback();
|
||||
return R.error(res, 'Task list not found.', 404);
|
||||
}
|
||||
|
||||
const startOrderIndex = await nextOrderIndex(Task, { task_list_id: taskListId }, t);
|
||||
|
||||
const createdTasks = await Task.bulkCreate(
|
||||
tasks.map((task, i) => ({
|
||||
task_list_id: taskListId,
|
||||
name: task.name,
|
||||
description: task.description,
|
||||
deadline: task.deadline || null,
|
||||
order_index: startOrderIndex + i,
|
||||
createdBy: req.user.user_id,
|
||||
updatedBy: req.user.user_id,
|
||||
})),
|
||||
{ transaction: t }
|
||||
);
|
||||
|
||||
const reqRows = createdTasks.flatMap((task, i) =>
|
||||
(tasks[i].requirements ?? []).map((r, j) => ({
|
||||
...r,
|
||||
task_id: task.task_id,
|
||||
order: r.order ?? j,
|
||||
reference_id: r.reference_id || null, // '' → null (UUID column)
|
||||
reference_label: r.reference_label || null, // '' → null
|
||||
link_url: normalizeUrl(r.link_url),
|
||||
link_label: r.link_label || null,
|
||||
createdBy: req.user.user_id,
|
||||
updatedBy: req.user.user_id,
|
||||
}))
|
||||
);
|
||||
if (reqRows.length) await TaskRequirement.bulkCreate(reqRows, { transaction: t });
|
||||
|
||||
await t.commit();
|
||||
|
||||
const taskIds = createdTasks.map((task) => task.task_id);
|
||||
const full = await Task.findAll({
|
||||
where: { task_id: { [Op.in]: taskIds } },
|
||||
attributes: { exclude: adminExclude },
|
||||
include: [{
|
||||
model: TaskRequirement,
|
||||
as: 'requirements',
|
||||
attributes: { exclude: adminExclude },
|
||||
order: [['order', 'ASC']],
|
||||
}, PREREQUISITE_INCLUDE],
|
||||
order: [['order_index', 'ASC']],
|
||||
});
|
||||
|
||||
// Heads-up only — never blocks creation. See getPreCompletedAssignees.
|
||||
const allRequirements = full.flatMap((task) =>
|
||||
(task.requirements ?? []).map((r) => ({ type: r.type, reference_id: r.reference_id, reference_label: r.reference_label }))
|
||||
);
|
||||
const warnings = await getPreCompletedAssignees(taskListId, allRequirements);
|
||||
|
||||
logActivity(req.user.user_id, 'bulk_create_tasks', {
|
||||
entityType: 'task_list',
|
||||
entityId: Number(taskListId),
|
||||
details: { count: createdTasks.length, task_ids: taskIds },
|
||||
});
|
||||
|
||||
return R.success(res, `${createdTasks.length} task(s) created successfully.`, {
|
||||
tasks: full.map((task) => task.toJSON()),
|
||||
warnings,
|
||||
}, 201);
|
||||
} catch (err) {
|
||||
await t.rollback();
|
||||
console.error('[ADMIN][BULK CREATE TASKS]', err);
|
||||
return R.error(res, 'Could not create tasks.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── UPDATE ───────────────────────────────────────────────────────────────────
|
||||
|
||||
exports.updateTask = async (req, res) => {
|
||||
|
||||
@@ -218,8 +218,17 @@ exports.streamAsset = async (req, res) => {
|
||||
const { token } = req.params;
|
||||
|
||||
// ── CORS ──────────────────────────────────────────────────────────────────
|
||||
const allowedOrigin = process.env.FRONTEND_URL ?? "http://localhost:5173";
|
||||
res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
||||
// Mirrors server.js's global cors() origin check (reflect against
|
||||
// ALLOWED_ORIGINS) instead of a single hardcoded FRONTEND_URL — a static
|
||||
// origin here silently overwrote the correct header the global middleware
|
||||
// already set, breaking any CORS-checked read (e.g. pdf.js's Range-header
|
||||
// fetch) whenever FRONTEND_URL drifted from the deployed frontend domain.
|
||||
// <img>/<video> tags were unaffected since opaque loads skip CORS checks.
|
||||
const allowedOrigins = (process.env.ALLOWED_ORIGINS || process.env.APP_URL || "*").split(",");
|
||||
const requestOrigin = req.headers.origin;
|
||||
if (requestOrigin && allowedOrigins.includes(requestOrigin)) {
|
||||
res.setHeader("Access-Control-Allow-Origin", requestOrigin);
|
||||
}
|
||||
res.setHeader("Access-Control-Allow-Credentials", "true");
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Range, Authorization");
|
||||
|
||||
@@ -24,6 +24,7 @@ router.get("/by-subscription", ctrl.getCoursesBySubscription);
|
||||
router.get("/units-flat", ctrl.getUnitsFlat);
|
||||
router.get("/lessons-flat", ctrl.getLessonsFlat);
|
||||
router.get("/quizzes-flat", ctrl.getQuizzesFlat);
|
||||
router.get("/uuid/:uuid/structure-counts", ctrl.getCourseStructureCounts);
|
||||
router.put("/order", ctrl.reorderCourses); // persist top-level course catalog ordering { course_ids }
|
||||
|
||||
// ── then :courseId ────────────────────────────────────────────────────────────
|
||||
|
||||
+29
-21
@@ -3,55 +3,63 @@ 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("/", sensitiveOpsLimiter, controller.createTaskList);
|
||||
router.post("/bulk-archive", sensitiveOpsLimiter, controller.bulkArchiveTaskLists);
|
||||
router.post("/bulk-restore", sensitiveOpsLimiter, controller.bulkRestoreTaskLists);
|
||||
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", sensitiveOpsLimiter, controller.updateTaskList);
|
||||
router.delete("/:taskListId", sensitiveOpsLimiter, controller.archiveTaskList);
|
||||
router.patch("/:taskListId", controller.updateTaskList);
|
||||
router.delete("/:taskListId", controller.archiveTaskList);
|
||||
router.delete("/:taskListId/permanent", sensitiveOpsLimiter, controller.permanentlyDeleteTaskList);
|
||||
router.patch("/:taskListId/restore", sensitiveOpsLimiter, controller.restoreTaskList);
|
||||
router.patch("/:taskListId/restore", controller.restoreTaskList);
|
||||
|
||||
// ─── Task List Groups ─────────────────────────────────────────────────────────
|
||||
router.get("/:taskListId/groups", controller.getTaskListGroups);
|
||||
router.put("/:taskListId/groups", sensitiveOpsLimiter, controller.syncGroups);
|
||||
router.post("/:taskListId/groups/assign", sensitiveOpsLimiter, controller.assignGroups);
|
||||
router.post("/:taskListId/groups/unassign", sensitiveOpsLimiter, controller.unassignGroups);
|
||||
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", sensitiveOpsLimiter, controller.createTask);
|
||||
router.post("/:taskListId/tasks/bulk-archive", sensitiveOpsLimiter, controller.bulkArchiveTasks);
|
||||
router.post("/:taskListId/tasks/bulk-restore", sensitiveOpsLimiter, controller.bulkRestoreTasks);
|
||||
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", sensitiveOpsLimiter, controller.reorderTasks);
|
||||
router.patch("/:taskListId/tasks/order", controller.reorderTasks);
|
||||
|
||||
// ─── Tasks (single) ───────────────────────────────────────────────────────────
|
||||
router.get("/:taskListId/tasks/:taskId", controller.getTask);
|
||||
router.patch("/:taskListId/tasks/:taskId", sensitiveOpsLimiter, controller.updateTask);
|
||||
router.delete("/:taskListId/tasks/:taskId", sensitiveOpsLimiter, controller.archiveTask);
|
||||
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", sensitiveOpsLimiter, controller.restoreTask);
|
||||
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", sensitiveOpsLimiter, completionController.bulkArchiveCompletions);
|
||||
router.post("/:taskListId/tasks/:taskId/completions/bulk-restore", sensitiveOpsLimiter, completionController.bulkRestoreCompletions);
|
||||
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", sensitiveOpsLimiter, completionController.reviewSubmission);
|
||||
router.delete("/:taskListId/tasks/:taskId/completions/:completionId", sensitiveOpsLimiter, completionController.archiveCompletion);
|
||||
router.patch("/:taskListId/tasks/:taskId/completions/:completionId/restore", sensitiveOpsLimiter, completionController.restoreCompletion);
|
||||
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;
|
||||
Reference in New Issue
Block a user