UNPKG

strapi-plugin-masterclass

Version:
1,803 lines 912 kB
import Stripe from "stripe"; import axios from "axios"; import { v4 } from "uuid"; import require$$1 from "crypto"; import require$$0$1 from "child_process"; import require$$0$2 from "os"; import require$$0$4 from "path"; import require$$0$3 from "fs"; import require$$0$5 from "assert"; import require$$2 from "events"; import require$$0$7 from "buffer"; import require$$0$6 from "stream"; import require$$2$1 from "util"; import require$$0$8 from "constants"; import "node:stream"; const bootstrap = ({ strapi: strapi2 }) => { if (!strapi2.plugins["mux-video-uploader"]) { throw new Error( "The Mux Video Uploader plugin is required. Please install strapi-plugin-mux-video-uploader." ); } if (!strapi2.plugins["seo"]) { throw new Error( "The @strapi/plugin-seo plugin is required. Please install @strapi/plugin-seo." ); } }; const destroy = ({ strapi: strapi2 }) => { }; const pluginId = "masterclass"; const CATEGORY_MODEL = `plugin::${pluginId}.mc-category`; const COURSE_MODEL = `plugin::${pluginId}.mc-course`; const LECTURE_MODEL = `plugin::${pluginId}.mc-lecture`; const MODULE_MODEL = `plugin::${pluginId}.mc-module`; const STUDENT_COURSE_MODEL = `plugin::${pluginId}.mc-student-course`; const ORDER_MODEL = `plugin::${pluginId}.mc-order`; const pageActions = ["create", "update", "delete"]; const LectureActions = { async create(context, strapi2) { let connectVideo = []; let connectModule = []; if (context.params.data.video) { const { connect } = context.params.data.video; connectVideo = connect ? connect : []; } if (context.params.data.module) { const { connect } = context.params.data.module; connectModule = connect ? connect : []; } if (!connectVideo.length) { context.params.data.duration = 0; return; } const videoDocumentId = connectVideo[0].documentId; const video = await strapi2.documents("plugin::mux-video-uploader.mux-asset").findOne({ documentId: videoDocumentId, fields: ["duration"] }); if (!video) { return; } const videoDuration = Math.floor(video.duration ? video.duration : 0); context.params.data.duration = videoDuration; if (!connectModule.length) { return; } const moduleDocumentId = connectModule[0].documentId; const module = await strapi2.documents(MODULE_MODEL).findOne({ documentId: moduleDocumentId, fields: ["duration"], populate: { course: { fields: ["duration", "documentId"] } } }); if (!module) { return; } await strapi2.documents(MODULE_MODEL).update({ documentId: moduleDocumentId, data: { duration: module.duration ? module.duration + videoDuration : videoDuration } }); const { course } = module; if (!course) { return; } await strapi2.documents(COURSE_MODEL).update({ documentId: course.documentId, data: { duration: course.duration ? course.duration + videoDuration : videoDuration } }); }, async update(context, strapi2) { let connectVideo = []; let disconnectVideo = []; let connectModule = []; let disconnectModule = []; if (context.params.data.video) { const { connect, disconnect } = context.params.data.video; connectVideo = connect ? connect : []; disconnectVideo = disconnect ? disconnect : []; } if (context.params.data.module) { const { connect, disconnect } = context.params.data.module; connectModule = connect ? connect : []; disconnectModule = disconnect ? disconnect : []; } let oldVideoDuration = context.params.data.duration ? context.params.data.duration : 0; let newVideoDuration = context.params.data.duration ? context.params.data.duration : 0; if (disconnectVideo.length > 0) { context.params.data.duration = 0; const videoDocumentId = disconnectVideo[0].documentId; const video = await strapi2.documents("plugin::mux-video-uploader.mux-asset").findOne({ documentId: videoDocumentId, fields: ["duration"] }); if (video) { oldVideoDuration = Math.floor(video.duration ? video.duration : 0); } } if (connectVideo.length > 0) { const videoDocumentId = connectVideo[0].documentId; const video = await strapi2.documents("plugin::mux-video-uploader.mux-asset").findOne({ documentId: videoDocumentId, fields: ["duration"] }); if (video) { const duration = Math.floor(video.duration ? video.duration : 0); context.params.data.duration = duration; newVideoDuration = duration; } } if (!connectModule.length && !disconnectModule.length) { return; } if (disconnectModule.length && oldVideoDuration > 0) { const moduleDocumentId = disconnectModule[0].documentId; const module = await strapi2.documents(MODULE_MODEL).findOne({ documentId: moduleDocumentId, fields: ["duration"], populate: { course: { fields: ["duration", "documentId"] } } }); if (module) { let newModuleDuration = 0; if (module.duration && module.duration - oldVideoDuration > 0) { newModuleDuration = module.duration - oldVideoDuration; } await strapi2.documents(MODULE_MODEL).update({ documentId: moduleDocumentId, data: { duration: newModuleDuration } }); const { course } = module; if (course) { let newCourseDuration = 0; if (course.duration && course.duration - oldVideoDuration > 0) { newCourseDuration = course.duration - oldVideoDuration; } await strapi2.documents(COURSE_MODEL).update({ documentId: course.documentId, data: { duration: newCourseDuration } }); } } } if (connectModule.length && newVideoDuration > 0) { const moduleDocumentId = connectModule[0].documentId; const module = await strapi2.documents(MODULE_MODEL).findOne({ documentId: moduleDocumentId, fields: ["duration"], populate: { course: { fields: ["duration", "documentId"] } } }); if (module) { let newModuleDuration = newVideoDuration; if (module.duration) { newModuleDuration = module.duration + newVideoDuration; } await strapi2.documents(MODULE_MODEL).update({ documentId: moduleDocumentId, data: { duration: newModuleDuration } }); const { course } = module; if (course) { let newCourseDuration = newVideoDuration; if (course.duration) { newCourseDuration = course.duration + newVideoDuration; } await strapi2.documents(COURSE_MODEL).update({ documentId: course.documentId, data: { duration: newCourseDuration } }); } } } }, // WARNING: This is not going to update the module or course durations correctly // if invoked from bulk delete from the content manages due to race conditions. async delete(context, strapi2) { const lecture = await strapi2.documents(LECTURE_MODEL).findOne({ documentId: context.params.documentId, fields: ["duration"], populate: { module: { fields: ["duration"], populate: { course: { fields: ["duration", "documentId"] } } } } }); const { module } = lecture; if (!module) { return; } const videoDuration = lecture.duration ? lecture.duration : 0; if (!videoDuration) { return; } let newModuleDuration = 0; if (module.duration && module.duration - videoDuration > 0) { newModuleDuration = module.duration - videoDuration; } await strapi2.documents(MODULE_MODEL).update({ documentId: module.documentId, data: { duration: newModuleDuration } }); const { course } = module; if (course) { let newCourseDuration = 0; if (course.duration && course.duration - videoDuration > 0) { newCourseDuration = course.duration - videoDuration; } await strapi2.documents(COURSE_MODEL).update({ documentId: course.documentId, data: { duration: newCourseDuration } }); } } }; const ModuleActions = { async create(context, strapi2) { context.params.data.duration = 0; let connectLectures = []; let connectCourse = []; if (context.params.data.lectures) { const { connect } = context.params.data.lectures; connectLectures = connect ? connect : []; } if (context.params.data.course) { const { connect } = context.params.data.course; connectCourse = connect ? connect : []; } if (!connectLectures.length) { return; } let totalLecturesDuration = 0; await Promise.all(connectLectures.map(async (connectLecture) => { const lectureDocumentId = connectLecture.documentId; const lecture = await strapi2.documents(LECTURE_MODEL).findOne({ documentId: lectureDocumentId, fields: ["duration"] }); if (lecture && lecture.duration > 0) { totalLecturesDuration += lecture.duration; } })); if (!(totalLecturesDuration > 0)) { return; } context.params.data.duration = totalLecturesDuration; if (!connectCourse.length) { return; } const courseDocumentId = connectCourse[0].documentId; const course = await strapi2.documents(COURSE_MODEL).findOne({ documentId: courseDocumentId, fields: ["duration"] }); if (!course) { return; } const newCourseDuration = course.duration ? course.duration + totalLecturesDuration : totalLecturesDuration; await strapi2.documents(COURSE_MODEL).update({ documentId: courseDocumentId, data: { duration: newCourseDuration } }); }, async update(context, strapi2) { let connectLectures = []; let disconnectLectures = []; let connectCourse = []; let disconnectCourse = []; if (context.params.data.lectures) { const { connect, disconnect } = context.params.data.lectures; connectLectures = connect ? connect : []; disconnectLectures = disconnect ? disconnect : []; } if (context.params.data.course) { const { connect, disconnect } = context.params.data.course; connectCourse = connect ? connect : []; disconnectCourse = disconnect ? disconnect : []; } let removedLecturesDuration = 0; let addedLecturesDuration = 0; if (disconnectLectures.length) { await Promise.all(disconnectLectures.map(async (disconnectLecture) => { const lectureDocumentId = disconnectLecture.documentId; const lecture = await strapi2.documents(LECTURE_MODEL).findOne({ documentId: lectureDocumentId, fields: ["duration"] }); if (lecture && lecture.duration > 0) { removedLecturesDuration += lecture.duration; } })); } if (connectLectures.length) { await Promise.all(connectLectures.map(async (connectLecture) => { const lectureDocumentId = connectLecture.documentId; const lecture = await strapi2.documents(LECTURE_MODEL).findOne({ documentId: lectureDocumentId, fields: ["duration"] }); if (lecture && lecture.duration > 0) { addedLecturesDuration += lecture.duration; } })); } const diffModuleDuration = addedLecturesDuration - removedLecturesDuration; const currentModuleDuration = context.params.data.duration ? context.params.data.duration : 0; const newModuleDuration = currentModuleDuration + diffModuleDuration; context.params.data.duration = newModuleDuration; if (disconnectCourse.length && currentModuleDuration > 0) { const courseDocumentId = disconnectCourse[0].documentId; const course = await strapi2.documents(COURSE_MODEL).findOne({ documentId: courseDocumentId, fields: ["duration"] }); if (course) { let newCourseDuration = course.duration ? course.duration - currentModuleDuration : 0; await strapi2.documents(COURSE_MODEL).update({ documentId: courseDocumentId, data: { duration: newCourseDuration } }); } } if (connectCourse.length) { const courseDocumentId = connectCourse[0].documentId; const course = await strapi2.documents(COURSE_MODEL).findOne({ documentId: courseDocumentId, fields: ["duration"] }); if (course) { let newCourseDuration = course.duration ? course.duration + newModuleDuration : newModuleDuration; await strapi2.documents(COURSE_MODEL).update({ documentId: courseDocumentId, data: { duration: newCourseDuration } }); } } if (disconnectCourse.length || connectCourse.length) { return; } const module = await strapi2.documents(MODULE_MODEL).findOne({ documentId: context.params.documentId, populate: { course: { fields: ["duration", "documentId"] } } }); if (module && module.course && diffModuleDuration != 0) { const { course } = module; let newCourseDuration = course.duration ? course.duration + diffModuleDuration : newModuleDuration; await strapi2.documents(COURSE_MODEL).update({ documentId: course.documentId, data: { duration: newCourseDuration } }); } }, // WARNING: This is not going to update the module or course durations correctly // if invoked from bulk delete from the content manages due to race conditions. async delete(context, strapi2) { const module = await strapi2.documents(MODULE_MODEL).findOne({ documentId: context.params.documentId, fields: ["duration"], populate: { course: { fields: ["duration", "documentId"] } } }); const { course } = module; if (!module) { return; } const moduleDuration = module.duration ? module.duration : 0; if (!moduleDuration) { return; } if (course) { let newCourseDuration = 0; if (course.duration && course.duration - moduleDuration > 0) { newCourseDuration = course.duration - moduleDuration; } await strapi2.documents(COURSE_MODEL).update({ documentId: course.documentId, data: { duration: newCourseDuration } }); } } }; const CourseActions = { async create(context, strapi2) { let connectModules = []; if (context.params.data.modules) { const { connect } = context.params.data.modules; connectModules = connect ? connect : []; } if (!connectModules.length) { context.params.data.duration = 0; return; } let totalModulesDuration = 0; await Promise.all(connectModules.map(async (connectModule) => { const moduleDocumentId = connectModule.documentId; const module = await strapi2.documents(MODULE_MODEL).findOne({ documentId: moduleDocumentId, fields: ["duration"] }); if (module && module.duration > 0) { totalModulesDuration += module.duration; } })); if (!(totalModulesDuration > 0)) { return; } context.params.data.duration = totalModulesDuration; }, async update(context, strapi2) { let connectModules = []; let disconnectModules = []; if (context.params.data.modules) { const { connect, disconnect } = context.params.data.modules; connectModules = connect ? connect : []; disconnectModules = disconnect ? disconnect : []; } if (!connectModules.length && !disconnectModules.length) { return; } let removedModulesDuration = 0; let addedModulesDuration = 0; if (disconnectModules.length) { await Promise.all(disconnectModules.map(async (disconnectModule) => { const moduleDocumentId = disconnectModule.documentId; const module = await strapi2.documents(MODULE_MODEL).findOne({ documentId: moduleDocumentId, fields: ["duration"] }); if (module && module.duration > 0) { removedModulesDuration += module.duration; } })); } if (connectModules.length) { await Promise.all(connectModules.map(async (connectModule) => { const moduleDocumentId = connectModule.documentId; const module = await strapi2.documents(MODULE_MODEL).findOne({ documentId: moduleDocumentId, fields: ["duration"] }); if (module && module.duration > 0) { addedModulesDuration += module.duration; } })); } const diffCourseDuration = addedModulesDuration - removedModulesDuration; const currentCourseDuration = context.params.data.duration ? context.params.data.duration : 0; const newCourseDuration = currentCourseDuration + diffCourseDuration; context.params.data.duration = newCourseDuration; }, async delete(context, strapi2) { } }; const registerDocServiceMiddleware = ({ strapi: strapi2 }) => { strapi2.documents.use(async (context, next) => { if (context.uid.endsWith("mc-lecture") && pageActions.includes(context.action)) { await LectureActions[context.action](context, strapi2); } if (context.uid.endsWith("mc-module") && pageActions.includes(context.action)) { await ModuleActions[context.action](context, strapi2); } if (context.uid.endsWith("mc-course") && pageActions.includes(context.action)) { await CourseActions[context.action](context, strapi2); } return next(); }); }; const register = ({ strapi: strapi2 }) => { const user = strapi2.contentType("plugin::users-permissions.user"); user.attributes = { // Spread previous defined attributes ...user.attributes, // Add new, or override attributes courses: { type: "relation", relation: "oneToMany", target: "plugin::masterclass.mc-student-course", mappedBy: "student" } }; registerDocServiceMiddleware({ strapi: strapi2 }); }; const config = { default: { stripeSecretKey: "", paypalClientId: "", paypalClientSecret: "", brandName: "", paypalReturnUrl: "", paypalCancelUrl: "", paypalProductionMode: false, callbackUrl: "", paymentMethods: ["card"], allowPromotionCodes: false, checkoutSuccessUrl: "", checkoutCancelUrl: "" }, validator(config2) { const missingConfigs = []; if (!config2.stripeSecretKey) { missingConfigs.push("accessTokenId"); } if (missingConfigs.length > 0) { throw new Error( `Please remember to set up the file based config for your plugin. Refer to the "Configuration" of the README for this plugin for additional details. Configs missing: ${missingConfigs.join(", ")}` ); } } }; const kind$6 = "collectionType"; const collectionName$6 = "mc_categories"; const info$6 = { singularName: "mc-category", pluralName: "mc-categories", displayName: "Category", description: "" }; const options$6 = { draftAndPublish: false }; const pluginOptions$1 = { "content-manager": { visible: true }, "content-type-builder": { visible: true } }; const attributes$6 = { title: { type: "string" }, description: { type: "blocks" }, thumbnail: { type: "media", multiple: false, required: false, allowedTypes: [ "images", "files" ] }, slug: { type: "uid", targetField: "title" }, courses: { type: "relation", relation: "oneToMany", target: "plugin::masterclass.mc-course", mappedBy: "category" } }; const mcCategory = { kind: kind$6, collectionName: collectionName$6, info: info$6, options: options$6, pluginOptions: pluginOptions$1, attributes: attributes$6 }; const kind$5 = "collectionType"; const collectionName$5 = "mc_courses"; const info$5 = { singularName: "mc-course", pluralName: "mc-courses", displayName: "Course", description: "" }; const options$5 = { draftAndPublish: false }; const attributes$5 = { title: { type: "string" }, duration: { type: "integer", configurable: false }, description: { type: "blocks" }, price: { type: "decimal" }, thumbnail: { type: "media", multiple: false, required: false, allowedTypes: [ "images", "files" ] }, long_description: { type: "blocks" }, difficulty: { type: "enumeration", "enum": [ "Beginner", "Intermediate", "Advanced" ] }, language: { type: "enumeration", "enum": [ "English", "Русский" ] }, category: { type: "relation", relation: "manyToOne", target: "plugin::masterclass.mc-category", inversedBy: "courses" }, slug: { type: "uid", targetField: "title" }, students: { type: "relation", relation: "oneToMany", target: "plugin::masterclass.mc-student-course", mappedBy: "course" }, modules: { type: "relation", relation: "oneToMany", target: "plugin::masterclass.mc-module", mappedBy: "course" }, instructor: { type: "relation", relation: "manyToOne", target: "plugin::masterclass.mc-instructor", inversedBy: "courses" }, seo: { type: "component", repeatable: false, component: "shared.seo" } }; const mcCourse = { kind: kind$5, collectionName: collectionName$5, info: info$5, options: options$5, attributes: attributes$5 }; const kind$4 = "collectionType"; const collectionName$4 = "mc_lectures"; const info$4 = { singularName: "mc-lecture", pluralName: "mc-lectures", displayName: "Lecture", description: "" }; const options$4 = { draftAndPublish: false }; const attributes$4 = { title: { type: "string" }, slug: { type: "uid", targetField: "title" }, duration: { type: "integer", configurable: false }, video: { type: "relation", relation: "oneToOne", target: "plugin::mux-video-uploader.mux-asset" }, module: { type: "relation", relation: "manyToOne", target: "plugin::masterclass.mc-module", inversedBy: "lectures" }, description: { type: "blocks" } }; const mcLecture = { kind: kind$4, collectionName: collectionName$4, info: info$4, options: options$4, attributes: attributes$4 }; const kind$3 = "collectionType"; const collectionName$3 = "mc_modules"; const info$3 = { singularName: "mc-module", pluralName: "mc-modules", displayName: "Module", description: "" }; const options$3 = { draftAndPublish: false }; const attributes$3 = { title: { type: "string" }, duration: { type: "integer", configurable: false }, course: { type: "relation", relation: "manyToOne", target: "plugin::masterclass.mc-course", inversedBy: "modules" }, lectures: { type: "relation", relation: "oneToMany", target: "plugin::masterclass.mc-lecture", mappedBy: "module" }, slug: { type: "uid", targetField: "title" }, description: { type: "blocks" } }; const mcModule = { kind: kind$3, collectionName: collectionName$3, info: info$3, options: options$3, attributes: attributes$3 }; const kind$2 = "collectionType"; const collectionName$2 = "mc_student_courses"; const info$2 = { singularName: "mc-student-course", pluralName: "mc-student-courses", displayName: "StudentCourse", description: "" }; const options$2 = { draftAndPublish: false }; const attributes$2 = { course: { type: "relation", relation: "manyToOne", target: "plugin::masterclass.mc-course", inversedBy: "students" }, student: { type: "relation", relation: "manyToOne", target: "plugin::users-permissions.user", inversedBy: "courses" }, current_lecture: { type: "relation", relation: "oneToOne", target: "plugin::masterclass.mc-lecture" }, lectures_completed: { type: "relation", relation: "oneToMany", target: "plugin::masterclass.mc-lecture" } }; const mcStudentCourse = { kind: kind$2, collectionName: collectionName$2, info: info$2, options: options$2, attributes: attributes$2 }; const kind$1 = "collectionType"; const collectionName$1 = "mc_order"; const info$1 = { singularName: "mc-order", pluralName: "mc-orders", displayName: "Order", description: "" }; const options$1 = { draftAndPublish: false, comment: "" }; const attributes$1 = { amount: { type: "decimal", configurable: false }, user: { type: "relation", relation: "oneToOne", target: "plugin::users-permissions.user", configurable: false }, confirmed: { type: "boolean", "default": false, configurable: false }, checkout_session: { type: "string", configurable: false }, payment_method: { type: "enumeration", "enum": [ "paypal", "credit_card" ], configurable: false }, items: { type: "json", configurable: false }, response: { type: "json", configurable: false }, courses: { type: "relation", relation: "oneToMany", target: "plugin::masterclass.mc-course", configurable: false } }; const mcOrder = { kind: kind$1, collectionName: collectionName$1, info: info$1, options: options$1, attributes: attributes$1 }; const kind = "collectionType"; const collectionName = "mc_instructors"; const info = { singularName: "mc-instructor", pluralName: "mc-instructors", displayName: "Instructor", description: "" }; const options = { draftAndPublish: false }; const pluginOptions = { "content-manager": { visible: true }, "content-type-builder": { visible: true } }; const attributes = { name: { type: "string" }, bio: { type: "blocks" }, image: { type: "media", multiple: false, required: false, allowedTypes: [ "images" ] }, slug: { type: "uid", targetField: "name" }, designation: { type: "string" }, courses: { type: "relation", relation: "oneToMany", target: "plugin::masterclass.mc-course", mappedBy: "instructor" } }; const mcInstructor = { kind, collectionName, info, options, pluginOptions, attributes }; const contentTypes = { "mc-category": { schema: mcCategory }, "mc-course": { schema: mcCourse }, "mc-lecture": { schema: mcLecture }, "mc-module": { schema: mcModule }, "mc-student-course": { schema: mcStudentCourse }, "mc-order": { schema: mcOrder }, "mc-instructor": { schema: mcInstructor } }; const controller = ({ strapi: strapi2 }) => ({ async find(ctx) { let courses = await strapi2.documents(COURSE_MODEL).findMany({ populate: { seo: { populate: { metaImage: { fields: ["alternativeText", "url"] }, openGraph: { populate: { ogImage: { fields: ["alternativeText", "url"] } } } } }, thumbnail: { fields: ["name", "url"] }, modules: { fields: ["title", "duration", "slug"], populate: { lectures: { fields: ["title", "duration", "slug", "description"] } } }, category: { fields: ["slug", "title", "id"] }, students: { fields: ["documentId"] }, instructor: { fields: ["name", "slug", "bio", "designation"], populate: { image: { fields: ["name", "url"] } } } } }); courses = courses.map((course) => { const totalLectures = course.modules.reduce((acc, module) => { return acc + module.lectures.length; }, 0); return { ...course, total_students: course.students.length, total_lectures: totalLectures }; }); ctx.body = { courses }; }, async findOne(ctx) { const { slug } = ctx.params; let course = await strapi2.documents(COURSE_MODEL).findFirst({ filters: { slug: { $eq: slug } }, populate: { thumbnail: { fields: ["name", "url"] }, modules: { fields: ["title", "duration", "description", "slug"], populate: { lectures: { fields: ["title", "duration", "slug", "description"] } } }, seo: { populate: { metaImage: { fields: ["alternativeText", "url"] }, openGraph: { populate: { ogImage: { fields: ["alternativeText", "url"] } } } } }, category: { fields: ["slug", "title", "id"] }, instructor: { fields: ["name", "slug", "bio", "designation"], populate: { image: { fields: ["name", "url"] } } }, students: { fields: ["documentId"] } } }); const totalLectures = course.modules.reduce((acc, module) => { return acc + module.lectures.length; }, 0); course = { ...course, total_students: course.students.length, total_lectures: totalLectures }; ctx.body = { course }; }, async findSlugs(ctx) { const courses = await strapi2.documents(COURSE_MODEL).findMany({ filters: {}, fields: ["slug"] }); ctx.body = { courses }; }, /* * Get the classes the user (if any) has marked as seen and the number of students */ async getCourseDetails(ctx) { const { user } = ctx.state; const { courseId } = ctx.params; let classesCompleted = []; if (user) { const student = await strapi2.documents(STUDENT_COURSE_MODEL).findFirst( { filters: { student: { documentId: { $eq: user.documentId } }, course: { documentId: { $eq: courseId } } }, populate: { lectures_completed: { fields: ["documentId", "slug"] }, current_lecture: { fields: ["documentId", "slug"] } } } ); if (student) { classesCompleted = student.lectures_completed; } } const students = await strapi2.documents(STUDENT_COURSE_MODEL).count({ filters: { course: { documentId: { $eq: courseId } } } }); ctx.body = { classesCompleted, students }; }, /* * Get user progress */ async getClassesCompleted(ctx) { const { user } = ctx.state; const { courseId } = ctx.params; if (!user) { return ctx.badRequest("There must be an user"); } const student = await strapi2.documents(STUDENT_COURSE_MODEL).findFirst( { filters: { student: { documentId: { $eq: user.documentId } }, course: { documentId: { $eq: courseId } } }, populate: { lectures_completed: { fields: ["id", "slug"] } } } ); if (!student) { return ctx.badRequest("No access to this course"); } ctx.body = { classesCompleted: student.lectures_completed }; }, /* * Get current lecture to resume course */ async getCurrentLecture(ctx) { const { user } = ctx.state; const { courseId } = ctx.params; if (!user) { return ctx.badRequest("There must be an user"); } const student = await strapi2.documents(STUDENT_COURSE_MODEL).findFirst( { filters: { student: { documentId: { $eq: user.documentId } }, course: { documentId: { $eq: courseId } } }, populate: { course: { populate: { modules: { populate: { lectures: { fields: ["documentId", "title", "description", "duration", "slug"], populate: { video: { fields: ["id", "asset_id"] } } } } } } }, current_lecture: { fields: ["id", "documentId", "title", "slug"] } } } ); if (!student) { return ctx.badRequest("No access to this course"); } const lectures = student.course.modules.reduce((lectures2, module) => { return lectures2.concat(module.lectures); }, []); if (!lectures.length) { return ctx.badRequest("This course does not have any lecture"); } const currentLecture = student.current_lecture || lectures[0]; return { currentLecture }; }, /* * Resume course */ async resumeCourse(ctx) { const { user } = ctx.state; const { courseId } = ctx.params; if (!user) { return ctx.badRequest("There must be an user"); } const student = await strapi2.documents(STUDENT_COURSE_MODEL).findFirst( { filters: { student: { documentId: { $eq: user.documentId } }, course: { documentId: { $eq: courseId } } }, populate: { course: { populate: { modules: { populate: { lectures: { populate: { video: { fields: ["id", "asset_id", "playback_id"] } } } } } } }, lectures_completed: { fields: ["id"] }, current_lecture: { fields: ["id"], populate: { video: { fields: ["asset_id", "playback_id"] } } } } } ); if (!student) { return ctx.badRequest("No access to this course"); } const lectures = student.course.modules.reduce((lectures2, module) => { return lectures2.concat(module.lectures); }, []); if (!lectures.length) { return ctx.badRequest("This course does not have any lecture"); } const currentLecture = student.current_lecture || lectures[0]; const signed = await strapi2.service("plugin::mux-video-uploader.mux").signPlaybackId(currentLecture.video.playback_id, "video"); const playbackID = currentLecture.video.playback_id; return { PlayAuth: `https://stream.mux.com/${playbackID}.m3u8?token=${signed.token}`, VideoId: playbackID, classesCompleted: student.lectures_completed, currentLectureID: currentLecture.id }; }, /* * Get play auth for the given lecture */ async getPlayAuth(ctx) { const { user } = ctx.state; if (!user) { return ctx.badRequest("There must be an user"); } const { courseId } = ctx.params; const { lecture } = ctx.query; const student = await strapi2.documents(STUDENT_COURSE_MODEL).findFirst( { filters: { student: { documentId: { $eq: user.documentId } }, course: { documentId: { $eq: courseId } } }, populate: { lectures_completed: { fields: ["id"] } } } ); if (!student) { return ctx.badRequest("No access to this course"); } const newCurrentLecture = await strapi2.documents(LECTURE_MODEL).findFirst( { filters: { slug: { $eq: lecture } }, populate: { video: { fields: ["asset_id", "playback_id"] } } } ); if (!newCurrentLecture) { return ctx.badRequest("The lecture does not exist"); } await strapi2.documents(STUDENT_COURSE_MODEL).update({ documentId: student.documentId, data: { current_lecture: newCurrentLecture.id } }); const signed = await strapi2.service("plugin::mux-video-uploader.mux").signPlaybackId(newCurrentLecture.video.playback_id, "video"); const playbackID = newCurrentLecture.video.playback_id; return { PlayAuth: `https://stream.mux.com/${playbackID}.m3u8?token=${signed.token}`, VideoId: newCurrentLecture.video.playback_id, classesCompleted: student.lectures_completed, currentLectureID: newCurrentLecture.id }; }, async checkLecture(ctx) { const { user } = ctx.state; const { courseId } = ctx.params; const { lecture_id } = ctx.request.body; const student = await strapi2.documents(STUDENT_COURSE_MODEL).findFirst( { filters: { student: { documentId: { $eq: user.documentId } }, course: { documentId: { $eq: courseId } } }, populate: { course: { populate: { modules: { populate: { lectures: { fields: ["id", "slug", "documentId"] } } } } }, lectures_completed: { fields: ["id", "slug", "documentId"] }, current_lecture: { fields: ["id", "slug", "documentId"] } } } ); if (!student) { return ctx.badRequest("No access to this course"); } const rawLectures = student.course.modules.reduce((lectures, module) => { return lectures.concat(module.lectures); }, []); if (!rawLectures.length) { return ctx.badRequest("This course does not have any lectures"); } const currentLectureIndex = rawLectures.findIndex((l) => l.documentId === lecture_id); if (currentLectureIndex < 0) { return ctx.badRequest("The lecture does not exist or does not belong to this course"); } let classesCompleted = student.lectures_completed; if (!classesCompleted || !classesCompleted.length) { classesCompleted = [lecture_id]; } else { const idx = classesCompleted.findIndex((l) => l.documentId === lecture_id); if (idx < 0) { classesCompleted.push(lecture_id); } } let newCurrentLecture = student.current_lecture; if (currentLectureIndex !== rawLectures.length - 1) { newCurrentLecture = rawLectures[currentLectureIndex + 1]; } else { newCurrentLecture = rawLectures[currentLectureIndex]; } await strapi2.documents(STUDENT_COURSE_MODEL).update( { documentId: student.documentId, data: { current_lecture: newCurrentLecture ? newCurrentLecture.id : null, lectures_completed: classesCompleted } } ); ctx.body = { ok: true }; }, // this handler only returns the IDs of all the courses purchased by the user async getItemsPurchased(ctx) { const { user } = ctx.state; if (!user) { return ctx.badRequest("There must be an user"); } const student = await strapi2.documents("plugin::users-permissions.user").findOne({ documentId: user.documentId, populate: { courses: { populate: { course: { fields: ["documentId"] } } } } }); let res = student; if (!student) { res = { courses: [] }; } ctx.body = res; }, // this handler returns the full information of all the courses purchased by the user async getMyLearning(ctx) { const { user } = ctx.state; if (!user) { return ctx.badRequest("There must be an user"); } const student = await strapi2.documents("plugin::users-permissions.user").findOne({ documentId: user.documentId, fields: [], populate: { courses: { populate: { current_lecture: { fields: ["slug"] }, lectures_completed: { fields: ["slug"] }, course: { fields: [ "documentId", "duration", "title", "description", "price", "slug" ], populate: { thumbnail: { fields: ["documentId", "name", "url"] }, modules: { fields: ["documentId", "title", "duration"], populate: { lectures: { fields: ["documentId", "title", "duration", "description"] } } }, category: { fields: ["documentId", "slug", "title"] } } } } } } }); let res = student; if (!student) { res = { courses: [] }; } ctx.body = res; } }); const courseQuery = { fields: "*", populate: { thumbnail: { fields: ["name", "url"] }, modules: { populate: { lectures: { fields: ["title", "duration"] } } } } }; const categories = ({ strapi: strapi2 }) => ({ async index(ctx) { const categories2 = await strapi2.documents(CATEGORY_MODEL).findMany({ populate: { thumbnail: { fields: ["name", "url"] }, courses: courseQuery } }); const result = await Promise.all(categories2.map(async (category) => { let courses_count = await strapi2.documents(COURSE_MODEL).count({ filters: { category: { id: { $eq: category.id } } } }); category.courses_count = courses_count; return category; })); const schema2 = strapi2.getModel(CATEGORY_MODEL); ctx.body = { categories: await strapi2.contentAPI.sanitize.output(result, schema2) }; }, async findOne(ctx) { const { document_id } = ctx.params; const category = await strapi2.documents(CATEGORY_MODEL).findOne({ documentId: document_id, populate: { thumbnail: { fields: ["name", "url"] }, courses: courseQuery } }); let courses_count = await strapi2.documents(COURSE_MODEL).count({ filters: { category: { id: { $eq: category.id } } } }); category.courses_count = courses_count; const schema2 = strapi2.getModel(CATEGORY_MODEL); ctx.body = { category: await strapi2.contentAPI.sanitize.output(category, schema2) }; } }); const PLUGIN_NAME = "masterclass"; const getConfig = async () => await strapi.config.get(`plugin::${PLUGIN_NAME}`); const getService = (name) => { const service = strapi.plugin(PLUGIN_NAME).service(name); return service; }; const orders = ({ strapi: strapi2 }) => ({ async find(ctx) { const { user } = ctx.state; if (!user) { return ctx.badRequest("User must be authenticated"); } const result = await strapi2.documents(ORDER_MODEL).findMany({ filters: { user } }); ctx.body = { orders: result }; }, /** * Retrieve an order by id, only if it belongs to the user */ async findOne(ctx) { const { id } = ctx.params; const { user } = ctx.state; if (!user) { return ctx.badRequest("User must be authenticated"); } if (!id) { return ctx.badRequest("Id is required"); } const order = await strapi2.documents(ORDER_MODEL).findOne({ documentId: id, populate: { user: { fields: ["id"] }, courses: true } }); if (order && order.user.id !== user.id) { return ctx.forbidden("This order does not belong to this user"); } ctx.body = { order }; }, async create(ctx) { const { courses, payment_method } = ctx.request.body; if (!courses || !courses.length) { return ctx.badRequest("no items received"); } if (!["credit_card", "paypal"].includes(payment_method)) { return ctx.badRequest("invalid payment_method: " + payment_method); } let { user } = ctx.state; if (user) { const student = await strapi2.documents(STUDENT_COURSE_MODEL).findFirst({ filters: { student: { documentId: { $eq: user.documentId } }, course: { documentId: { $in: courses } } } }); if (student) { ctx.body = {}; return ctx.badRequest("user already purchased this course", { redirectToLogin: true }); } } const _courses = await strapi2.documents(COURSE_MODEL).findMany({ filters: { documentId: { $in: courses } } }); if (!_courses.length) { return ctx.badRequest("courses not found"); } const params = { user, payment_method, courses: _courses }; let result; try { result = await getService("payments").create(params); if (result.error) { return ctx[result.status](result.msg); } } catch (err) { console.log("orders create error:", err); return ctx.internalServerError("something went wrong"); } ctx.body = result; }, async confirmWithUser(ctx) { const { checkout_session } = ctx.request.body; if (!checkout_session) { return ctx.badRequest("checkout_session is required"); } let { user } = ctx.state; let order = await strapi2.documents(ORDER_MODEL).findFirst({ filters: { checkout_session: { $eq: checkout_session } }, populate: { user: { fields: ["id", "email"] }, courses: courseQuery } }); if (!order) { return ctx.badRequest("order not found"); } const email = order.user.email; if (user.email != email) { return ctx.badRequest("unmatched user and order email"); } const { courses } = order; if (order.confirmed) { ctx.body = { courses, is_new_account: false, user_email: user.email, checkout_session: order.checkout_session, id: order.id, documentId: order.documentId, amount: order.amount, confirmed: order.confirmed, payment_method: order.payment_method, createdAt: order.createdAt, publishedAt: order.publishedAt, updatedAt: order.updatedAt }; return; } try { const params = { checkout_session }; const result = await getService("payments").confirm(params); if (result.error) { return ctx[result.status](result.msg); } order = result; } catch (err) { console.log(err); return ctx.internalServerError("something went wrong"); } if (!order.confirmed) { return ctx.badRequest("could not confirm payment"); } await getService("courses").signIntoMultipleCourses({ user, courses }); ctx.body = { courses, is_new_account: false, user_email: user.email, checkout_session: order.checkout_session, id: order.id, documentId: order.documentId, amount: order.amount, confirmed: order.confirmed, payment_method: order.payment_method, createdAt: order.createdAt, publishedAt: order.publishedAt, updatedAt: order.updatedAt }; }, async confirm(ctx) { const { checkout_session } = ctx.request.body; if (!checkout_session) { return ctx.badRequest("checkout_session is required"); } let order = await strapi2.documents(ORDER_MODEL).findFirst({ filters: { checkout_session: { $eq: checkout_session } }, populate: { user: { fields: ["id", "confirmed", "email"] }, courses: courseQuery } }); if (!order) { return ctx.badRequest("order not found"); } let { user } = order; if (!user) { user = { confirmed: false, email: "" }; } const { courses } = order; if (order.confirmed) { ctx.body = { courses: courses.map((c) => ({ slug: c.slug, id: c.id, documentId: c.documentId })), is_new_account: !user.confirmed, user_email: user.email, checkout_session: order.checkout_session, id: order.id, documentId: order.documentId, amount: order.amount, confirmed: order.confirmed, payment_method: order.payment_method, createdAt: order.createdAt, publishedAt: order.publishedAt, updatedAt: order.updatedAt }; return; } try { const params = { checkout_session }; const result = await getService("payments").confirm(params); if (result.error) { return ctx[result.status](result.msg); } order = result; user = order.user; } catch (err) { console.log(err); return ctx.internalServerError("something went wrong"); } if (!order.confirmed) { return ctx.badRequest("could not confirm payment"); } if (order.user && courses.length > 0) { await getService("courses").signIntoMultipleCourses({ user: order.user, courses }); } ctx.body = { courses: courses.map((c) => ({ slug: c.slug, id: c.id, documentId: c.documentId })), is_new_account: !