Spaces:
Running
Running
File size: 5,110 Bytes
4c6b1e8 6e13570 4c6b1e8 6e13570 4c6b1e8 6e13570 4c6b1e8 6e13570 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | 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;
|