mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -112,8 +112,9 @@ async function buildUserContext(user_id) {
|
||||
// Returns true → user may access the course.
|
||||
// Returns false → user's tier is too low AND no valid individual purchase.
|
||||
async function canAccessCourse(user_id, course_id) {
|
||||
const course = await Course.findOne({ where: { course_id, ...notDeleted }, attributes: ['subscription'] });
|
||||
const course = await Course.findOne({ where: { course_id, ...notDeleted }, attributes: ['subscription', 'status'] });
|
||||
if (!course) return false;
|
||||
if (course.status !== 'published') return false;
|
||||
|
||||
const userCtx = await buildUserContext(user_id);
|
||||
|
||||
@@ -156,7 +157,15 @@ async function canAccessUnit(user_id, unit_id) {
|
||||
if (allowed) return true;
|
||||
}
|
||||
|
||||
const links = await CourseUnit.findAll({ where: { unit_id }, attributes: ['course_id'] });
|
||||
// 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),
|
||||
// matching the client discovery-list's course_count computation.
|
||||
const links = await CourseUnit.findAll({
|
||||
where: { unit_id },
|
||||
attributes: ['course_id'],
|
||||
include: [{ model: Course, as: 'course', attributes: [], where: { status: 'published', ...notDeleted }, required: true }],
|
||||
});
|
||||
if (!links.length) return !unit?.subscription;
|
||||
for (const link of links) {
|
||||
if (await canAccessCourse(user_id, link.course_id)) return true;
|
||||
@@ -256,7 +265,7 @@ exports.getCourses = async (req, res) => {
|
||||
};
|
||||
|
||||
const courses = await Course.findAll({
|
||||
where: { ...notDeleted },
|
||||
where: { ...notDeleted, status: 'published' },
|
||||
attributes: COURSE_LIST_ATTRS,
|
||||
include: [
|
||||
{
|
||||
|
||||
@@ -17,10 +17,11 @@
|
||||
* be able to access at least one attached course. Lessons resolve through
|
||||
* their parent units the same way.
|
||||
*
|
||||
* Discovery rule (getUnits/getLessons only): only course-free content is
|
||||
* listed at all — a unit with any course affiliation, or a lesson with any
|
||||
* unit that has a course affiliation, is excluded outright rather than
|
||||
* listed-but-locked. This does not affect the single-item endpoints above
|
||||
* Discovery rule (getUnits/getLessons only): ALL non-deleted units/lessons
|
||||
* are listed, whether or not they're attached to a course — course_count/
|
||||
* courses[] (published courses only) and is_locked tell the learner whether
|
||||
* a given item is standalone or bound, and if bound, whether they already
|
||||
* have access. This does not affect the single-item endpoints above
|
||||
* (:uuid) — those still enforce access normally for direct links, and
|
||||
* course-scoped consumption runs through a separate controller entirely.
|
||||
*
|
||||
@@ -61,12 +62,11 @@ function sanitizeQuestions(questions = []) {
|
||||
|
||||
// ─── UNIT LIBRARY (learner view) ──────────────────────────────────────────────
|
||||
|
||||
// Client-side Units/Lessons browsing only ever shows INDEPENDENT content —
|
||||
// anything affiliated with a course (directly, or for a lesson, through any
|
||||
// of its attached units) is excluded from these listings entirely, not just
|
||||
// flagged locked. This does not affect course-scoped consumption (which runs
|
||||
// through ClientCoursesContext/getCourse, a separate path) or direct-link
|
||||
// access to UnitDetails/LessonDetails, which still enforce access normally.
|
||||
// Client-side Units/Lessons browsing shows ALL content, bound to a course or
|
||||
// not — course_count/courses[] + is_locked below tell the learner which is
|
||||
// which. This does not affect course-scoped consumption (which runs through
|
||||
// ClientCoursesContext/getCourse, a separate path) or direct-link access to
|
||||
// UnitDetails/LessonDetails, which still enforce access normally.
|
||||
exports.getUnits = async (req, res) => {
|
||||
try {
|
||||
const rows = await sequelize.query(`
|
||||
@@ -76,17 +76,12 @@ exports.getUnits = async (req, res) => {
|
||||
JOIN lessons l ON l.lesson_id = ul.lesson_id AND l."deletedAt" IS NULL
|
||||
WHERE ul.unit_id = u.unit_id) AS lesson_count,
|
||||
(SELECT CAST(COUNT(*) AS INTEGER) FROM course_units cu
|
||||
JOIN courses c ON c.course_id = cu.course_id AND c."deletedAt" IS NULL
|
||||
JOIN courses c ON c.course_id = cu.course_id AND c."deletedAt" IS NULL AND c.status = 'published'
|
||||
WHERE cu.unit_id = u.unit_id) AS course_count,
|
||||
(SELECT quiz_id FROM unit_quizzes q
|
||||
WHERE q.unit_id = u.unit_id AND q."deletedAt" IS NULL LIMIT 1) AS quiz_id
|
||||
FROM units u
|
||||
WHERE u."deletedAt" IS NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM course_units cu
|
||||
JOIN courses c ON c.course_id = cu.course_id AND c."deletedAt" IS NULL
|
||||
WHERE cu.unit_id = u.unit_id
|
||||
)
|
||||
ORDER BY u.title ASC
|
||||
`, { type: sequelize.QueryTypes.SELECT });
|
||||
|
||||
@@ -97,7 +92,7 @@ exports.getUnits = async (req, res) => {
|
||||
const courseLinkRows = unitIds.length ? await sequelize.query(`
|
||||
SELECT cu.unit_id, c.course_id, c.uuid, c.title, c.subscription
|
||||
FROM course_units cu
|
||||
JOIN courses c ON c.course_id = cu.course_id AND c."deletedAt" IS NULL
|
||||
JOIN courses c ON c.course_id = cu.course_id AND c."deletedAt" IS NULL AND c.status = 'published'
|
||||
WHERE cu.unit_id IN (:unitIds)
|
||||
`, { replacements: { unitIds }, type: sequelize.QueryTypes.SELECT }) : [];
|
||||
|
||||
@@ -141,12 +136,6 @@ exports.getLessons = async (req, res) => {
|
||||
WHERE ul.lesson_id = l.lesson_id) AS unit_count
|
||||
FROM lessons l
|
||||
WHERE l."deletedAt" IS NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM unit_lessons ul
|
||||
JOIN course_units cu ON cu.unit_id = ul.unit_id
|
||||
JOIN courses c ON c.course_id = cu.course_id AND c."deletedAt" IS NULL
|
||||
WHERE ul.lesson_id = l.lesson_id
|
||||
)
|
||||
ORDER BY l.title ASC
|
||||
`, { type: sequelize.QueryTypes.SELECT });
|
||||
|
||||
@@ -157,7 +146,7 @@ exports.getLessons = async (req, res) => {
|
||||
SELECT DISTINCT ul.lesson_id, c.course_id, c.uuid, c.title, c.subscription
|
||||
FROM unit_lessons ul
|
||||
JOIN course_units cu ON cu.unit_id = ul.unit_id
|
||||
JOIN courses c ON c.course_id = cu.course_id AND c."deletedAt" IS NULL
|
||||
JOIN courses c ON c.course_id = cu.course_id AND c."deletedAt" IS NULL AND c.status = 'published'
|
||||
WHERE ul.lesson_id IN (:lessonIds)
|
||||
`, { replacements: { lessonIds }, type: sequelize.QueryTypes.SELECT }) : [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user