Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-08-13 19:58:52 +08:00
parent 9018d6d158
commit 2e9c2ad43f
23 changed files with 954 additions and 287 deletions
+9 -43
View File
@@ -158,8 +158,6 @@ async function canAccessUnit(user_id, unit_id) {
const unit = await Unit.findOne({ where: { unit_id, ...notDeleted }, attributes: ['subscription'] });
if (unit?.subscription && await hasItemGrant(user_id, 'unit', unit_id)) return true;
if (await hasActivePurchase(user_id, 'unit', unit_id)) return true;
// Only links to PUBLISHED courses count as a real course dependency — a unit
// whose only link is to a draft/unpublished course behaves as if it had no
// course link at all (falls through to the free/standalone branch below),
@@ -182,8 +180,6 @@ async function canAccessLesson(user_id, lesson_id) {
const lesson = await Lesson.findOne({ where: { lesson_id, ...notDeleted }, attributes: ['subscription'] });
if (lesson?.subscription && await hasItemGrant(user_id, 'lesson', lesson_id)) return true;
if (await hasActivePurchase(user_id, 'lesson', lesson_id)) return true;
const unitLinks = await UnitLesson.findAll({ where: { lesson_id }, attributes: ['unit_id'] });
if (!unitLinks.length) return !lesson?.subscription || lesson.subscription === 'free';
for (const link of unitLinks) {
@@ -1311,12 +1307,11 @@ exports.getLessonsByUnitUuid = async (req, res) => {
if (!await canAccessUnit(req.user.user_id, unit.unit_id)) {
const first = unit.courses?.[0] ?? null;
const { product, has_purchased, purchase_eligible } = await buildCheckoutInfo(req.user.user_id, "unit", unit);
return res.status(403).json({
status: "error",
message: "You do not have access to this unit.",
course: first ? { title: first.title, subscription: first.subscription } : null,
item: { uuid: unit.uuid, subscription: unit.subscription, product, has_purchased, purchase_eligible },
item: { uuid: unit.uuid, subscription: unit.subscription },
});
}
@@ -1440,12 +1435,11 @@ exports.getLessonByUuid = async (req, res) => {
if (!await canAccessLesson(req.user.user_id, lesson.lesson_id)) {
const firstCourse = lesson.units?.[0]?.courses?.[0] ?? null;
const { product, has_purchased, purchase_eligible } = await buildCheckoutInfo(req.user.user_id, "lesson", lesson);
return res.status(403).json({
status: "error",
message: "You do not have access to this lesson.",
course: firstCourse ? { title: firstCourse.title, subscription: firstCourse.subscription } : null,
item: { uuid: lesson.uuid, subscription: lesson.subscription, product, has_purchased, purchase_eligible },
item: { uuid: lesson.uuid, subscription: lesson.subscription },
});
}
@@ -1491,18 +1485,16 @@ exports.getLessonByUuid = async (req, res) => {
}
};
// ─── CHECKOUT INFO (course/unit/lesson) ───────────────────────────────────────
// Deliberately does NOT hard-403 on locked content like getCourse/
// getUnitByUuid/getLessonByUuid do — a locked-and-unpurchased item is exactly
// who needs to land on this page and see title/description/product, so it
// can't gate on the same canAccess*() check those content-serving routes use.
// Auth-only; content stays fully protected behind the routes above.
const CHECKOUT_PK = { course: "course_id", unit: "unit_id", lesson: "lesson_id" };
// ─── CHECKOUT INFO (course) ────────────────────────────────────────────────
// Deliberately does NOT hard-403 on locked content like getCourse does — a
// locked-and-unpurchased course is exactly who needs to land on this page and
// see title/description/product, so it can't gate on the same canAccess*()
// check those content-serving routes use. Auth-only; content stays fully
// protected behind the routes above.
async function buildCheckoutInfo(user_id, purchasable_type, record) {
const product = await mdl_Product.findOne({
where: { purchasable_type, purchasable_id: record[CHECKOUT_PK[purchasable_type]], is_active: true },
where: { purchasable_type, purchasable_id: record.course_id, is_active: true },
attributes: ["id", "name", "price", "currency", "access_days"],
});
const hasPurchase = product && await mdl_CoursePurchase.findOne({
@@ -1528,30 +1520,4 @@ exports.getCourseCheckoutInfo = async (req, res) => {
console.error("[CLIENT][COURSES][CHECKOUT INFO]", err);
return R.error(res, "Could not retrieve checkout info.", 500);
}
};
exports.getUnitCheckoutInfo = async (req, res) => {
try {
const { uuid } = req.params;
const unit = await Unit.findOne({ where: { uuid, ...notDeleted }, attributes: ["unit_id", "uuid", "title", "description", "subscription"] });
if (!unit) return R.error(res, "Unit not found.", 404);
const { product, has_purchased, purchase_eligible } = await buildCheckoutInfo(req.user.user_id, "unit", unit);
return R.success(res, "Checkout info retrieved.", { ...unit.toJSON(), product, has_purchased, purchase_eligible });
} catch (err) {
console.error("[CLIENT][UNITS][CHECKOUT INFO]", err);
return R.error(res, "Could not retrieve checkout info.", 500);
}
};
exports.getLessonCheckoutInfo = async (req, res) => {
try {
const { uuid } = req.params;
const lesson = await Lesson.findOne({ where: { uuid, ...notDeleted }, attributes: ["lesson_id", "uuid", "title", "description", "subscription"] });
if (!lesson) return R.error(res, "Lesson not found.", 404);
const { product, has_purchased, purchase_eligible } = await buildCheckoutInfo(req.user.user_id, "lesson", lesson);
return R.success(res, "Checkout info retrieved.", { ...lesson.toJSON(), product, has_purchased, purchase_eligible });
} catch (err) {
console.error("[CLIENT][LESSONS][CHECKOUT INFO]", err);
return R.error(res, "Could not retrieve checkout info.", 500);
}
};