const mongoose = require("mongoose"); const CATEGORIES = [ "Artificial Intelligence", "Cloud Computing", "Quantum Computing", "Cyber Security", "Networking", "Systems Engineering", "Professional Ethics", "Robotics", ]; const lessonSchema = new mongoose.Schema( { title: { type: String, required: true, trim: true, maxlength: 200 }, description: { type: String, default: "", maxlength: 1000 }, type: { type: String, enum: ["video", "document"], required: true }, order: { type: Number, default: 0 }, // Free preview lessons can be viewed without purchasing the course // (e.g. an intro video) — everything else requires confirmed access. isPreview: { type: Boolean, default: false }, // Cloudinary storage details. Uploaded with delivery type // "authenticated" so files can NEVER be fetched by guessing/sharing // the public ID — every playback URL must be freshly signed // server-side (see utils/cloudinaryUpload.js) for a user who is // confirmed to have access. These are plain (selected-by-default) // fields — deliberately NOT using Mongoose's `select: false` here, // because that projection option is unreliable two levels deep // inside an array-of-subdocuments-within-an-array-of-subdocuments // (curriculum[].lessons[].field) across Mongoose versions. The real // security boundary is that every response is built through // toCurriculumJSON()/toLockedCurriculumJSON() below, which simply // never copies these fields into what gets sent to the client. cloudinaryPublicId: { type: String, required: true }, cloudinaryResourceType: { type: String, enum: ["video", "raw", "image"], required: true }, format: { type: String, default: "" }, bytes: { type: Number, default: 0 }, durationSeconds: { type: Number, default: 0 }, }, { timestamps: true } ); const sectionSchema = new mongoose.Schema( { title: { type: String, required: true, trim: true, maxlength: 200 }, order: { type: Number, default: 0 }, lessons: { type: [lessonSchema], default: [] }, }, { timestamps: true } ); const courseSchema = new mongoose.Schema( { title: { type: String, required: true, trim: true, maxlength: 200 }, slug: { type: String, required: true, unique: true, lowercase: true, trim: true, index: true }, price: { type: Number, required: true, min: 0 }, currency: { type: String, default: "USD", uppercase: true, minlength: 3, maxlength: 3 }, duration: { type: String, required: true, trim: true }, level: { type: String, required: true, trim: true }, description: { type: String, required: true }, category: { type: String, enum: CATEGORIES, required: true, index: true }, // Marketing thumbnail shown on the homepage "Our Academy" preview and // on the Academy catalogue page. Uploaded by an admin as an actual // image file (see content.controller.js -> uploadThumbnail), stored // in Cloudinary as a plain PUBLIC asset (type: "upload") — unlike // lesson videos/documents, a thumbnail is meant to be publicly // visible to any visitor browsing the storefront, so it deliberately // does NOT use the authenticated/signed-URL scheme those use. image: { type: String, default: "" }, thumbnailPublicId: { type: String, default: "" }, highlights: { type: [String], default: [] }, isPublished: { type: Boolean, default: true }, // Study content, uploaded exclusively by admins (see content.controller.js). curriculum: { type: [sectionSchema], default: [] }, }, { timestamps: true } ); /** Curriculum view for enrolled users / admins: structure + metadata, no storage internals. */ courseSchema.methods.toCurriculumJSON = function toCurriculumJSON() { return this.curriculum .slice() .sort((a, b) => a.order - b.order) .map((section) => ({ id: section._id, title: section.title, order: section.order, lessons: section.lessons .slice() .sort((a, b) => a.order - b.order) .map((lesson) => ({ id: lesson._id, title: lesson.title, description: lesson.description, type: lesson.type, order: lesson.order, isPreview: lesson.isPreview, durationSeconds: lesson.durationSeconds, })), })); }; /** Locked view for logged-in users who have NOT purchased the course: titles only, previews flagged. */ courseSchema.methods.toLockedCurriculumJSON = function toLockedCurriculumJSON() { return this.toCurriculumJSON().map((section) => ({ ...section, lessons: section.lessons.map((lesson) => ({ id: lesson.id, title: lesson.title, type: lesson.type, order: lesson.order, isPreview: lesson.isPreview, durationSeconds: lesson.durationSeconds, locked: !lesson.isPreview, })), })); }; module.exports = mongoose.model("Course", courseSchema); module.exports.CATEGORIES = CATEGORIES;