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;