Spaces:
Running
Running
Download src/controllers/content.controller.js from AndreaNotes1/auth-app-api: direct link, hf CLI and curl.
- Browser
- Download file 12.1 kB
-
https://huggingface.co/spaces/AndreaNotes1/auth-app-api/resolve/main/src/controllers/content.controller.js
- Command line
-
hf download hf://spaces/AndreaNotes1/auth-app-api/src/controllers/content.controller.js
-
curl -L -o content.controller.js https://huggingface.co/spaces/AndreaNotes1/auth-app-api/resolve/main/src/controllers/content.controller.js
12.1 kB
| const fs = require("fs/promises"); | |
| const mongoose = require("mongoose"); | |
| const Course = require("../models/Course"); | |
| const AuditLog = require("../models/AuditLog"); | |
| const { | |
| uploadFromPath, | |
| deleteAsset, | |
| getSignedPlaybackUrl, | |
| resourceTypeForMimetype, | |
| uploadThumbnail: uploadThumbnailToCloudinary, | |
| deleteThumbnail, | |
| } = require("../utils/cloudinaryUpload"); | |
| const logger = require("../utils/logger"); | |
| function userHasAccess(user, courseId) { | |
| if (!user) return false; | |
| if (user.role === "admin") return true; | |
| return user.hasAccessToCourse(courseId); | |
| } | |
| // --------------------------------------------------------------------- | |
| // Learner-facing: view curriculum structure, and request a signed, | |
| // time-limited playback URL for one lesson at a time. | |
| // --------------------------------------------------------------------- | |
| async function getCourseCurriculum(req, res, next) { | |
| try { | |
| const course = await Course.findById(req.params.id); | |
| if (!course || !course.isPublished) return res.status(404).json({ error: "Course not found." }); | |
| const hasAccess = userHasAccess(req.user, course._id); | |
| res.json({ | |
| courseId: course._id, | |
| title: course.title, | |
| hasAccess, | |
| curriculum: hasAccess ? course.toCurriculumJSON() : course.toLockedCurriculumJSON(), | |
| }); | |
| } catch (err) { | |
| next(err); | |
| } | |
| } | |
| async function getLessonAccess(req, res, next) { | |
| try { | |
| const { id: courseId, sectionId, lessonId } = req.params; | |
| const course = await Course.findById(courseId); | |
| if (!course || !course.isPublished) return res.status(404).json({ error: "Course not found." }); | |
| const section = course.curriculum.id(sectionId); | |
| const lesson = section?.lessons.id(lessonId); | |
| if (!section || !lesson) return res.status(404).json({ error: "Lesson not found." }); | |
| const hasAccess = userHasAccess(req.user, course._id); | |
| if (!hasAccess && !lesson.isPreview) { | |
| return res.status(403).json({ error: "Purchase this course to access this lesson." }); | |
| } | |
| const { url, expiresAt } = getSignedPlaybackUrl(lesson.cloudinaryPublicId, lesson.cloudinaryResourceType, { | |
| format: lesson.format, | |
| viewer: { email: req.user.email }, | |
| }); | |
| await AuditLog.create({ | |
| actor: req.user._id, | |
| actorEmail: req.user.email, | |
| action: "lesson.access_granted", | |
| targetType: "Lesson", | |
| targetId: lesson._id, | |
| ip: req.ip, | |
| userAgent: req.headers["user-agent"], | |
| metadata: { courseId: course._id.toString() }, | |
| }); | |
| res.json({ | |
| url, | |
| expiresAt, | |
| type: lesson.type, | |
| title: lesson.title, | |
| }); | |
| } catch (err) { | |
| next(err); | |
| } | |
| } | |
| // --------------------------------------------------------------------- | |
| // Admin-only: manage curriculum structure and upload content to | |
| // Cloudinary. No learner-facing route ever exposes an upload capability. | |
| // --------------------------------------------------------------------- | |
| async function createSection(req, res, next) { | |
| try { | |
| const { title } = req.body; | |
| const course = await Course.findById(req.params.id); | |
| if (!course) return res.status(404).json({ error: "Course not found." }); | |
| const order = course.curriculum.length; | |
| course.curriculum.push({ title, order, lessons: [] }); | |
| await course.save(); | |
| res.status(201).json({ curriculum: course.toCurriculumJSON() }); | |
| } catch (err) { | |
| next(err); | |
| } | |
| } | |
| async function updateSection(req, res, next) { | |
| try { | |
| const { title, order } = req.body; | |
| const course = await Course.findById(req.params.id); | |
| if (!course) return res.status(404).json({ error: "Course not found." }); | |
| const section = course.curriculum.id(req.params.sectionId); | |
| if (!section) return res.status(404).json({ error: "Section not found." }); | |
| if (title !== undefined) section.title = title; | |
| if (order !== undefined) section.order = order; | |
| await course.save(); | |
| res.json({ curriculum: course.toCurriculumJSON() }); | |
| } catch (err) { | |
| next(err); | |
| } | |
| } | |
| async function deleteSection(req, res, next) { | |
| try { | |
| const course = await Course.findById(req.params.id); | |
| if (!course) return res.status(404).json({ error: "Course not found." }); | |
| const section = course.curriculum.id(req.params.sectionId); | |
| if (!section) return res.status(404).json({ error: "Section not found." }); | |
| // Clean up every Cloudinary asset in this section before removing it. | |
| await Promise.all( | |
| section.lessons.map((lesson) => | |
| deleteAsset(lesson.cloudinaryPublicId, lesson.cloudinaryResourceType).catch((err) => | |
| logger.error("Failed to delete Cloudinary asset", { error: err.message, lessonId: lesson._id.toString() }) | |
| ) | |
| ) | |
| ); | |
| section.deleteOne(); | |
| await course.save(); | |
| res.json({ curriculum: course.toCurriculumJSON() }); | |
| } catch (err) { | |
| next(err); | |
| } | |
| } | |
| async function uploadLesson(req, res, next) { | |
| const tempFilePath = req.file?.path; | |
| try { | |
| const { title, description, isPreview, order } = req.body; | |
| if (!req.file) return res.status(400).json({ error: "A file is required." }); | |
| const course = await Course.findById(req.params.id); | |
| if (!course) return res.status(404).json({ error: "Course not found." }); | |
| const section = course.curriculum.id(req.params.sectionId); | |
| if (!section) return res.status(404).json({ error: "Section not found." }); | |
| const resourceType = resourceTypeForMimetype(req.file.mimetype); | |
| const lessonType = resourceType === "video" ? "video" : "document"; | |
| const result = await uploadFromPath(tempFilePath, { | |
| resourceType, | |
| filenameHint: title, | |
| }); | |
| // From here on, the asset exists in Cloudinary. If anything below | |
| // fails (DB validation, a save error, etc.), we must delete it — | |
| // otherwise a failed attempt leaves an orphaned file in storage | |
| // forever, since nothing in the database ever references it to | |
| // clean it up later. | |
| try { | |
| section.lessons.push({ | |
| title, | |
| description: description || "", | |
| type: lessonType, | |
| order: order !== undefined ? Number(order) : section.lessons.length, | |
| isPreview: isPreview === "true" || isPreview === true, | |
| cloudinaryPublicId: result.public_id, | |
| cloudinaryResourceType: result.resource_type, | |
| format: result.format || "", | |
| bytes: result.bytes || 0, | |
| durationSeconds: result.duration ? Math.round(result.duration) : 0, | |
| }); | |
| await course.save(); | |
| await AuditLog.create({ | |
| actor: req.user._id, | |
| actorEmail: req.user.email, | |
| action: "admin.lesson_uploaded", | |
| targetType: "Course", | |
| targetId: course._id, | |
| ip: req.ip, | |
| userAgent: req.headers["user-agent"], | |
| metadata: { sectionId: section._id.toString(), title }, | |
| }); | |
| res.status(201).json({ curriculum: course.toCurriculumJSON() }); | |
| } catch (err) { | |
| await deleteAsset(result.public_id, result.resource_type).catch((cleanupErr) => | |
| logger.error("Failed to clean up orphaned Cloudinary asset after failed save", { | |
| error: cleanupErr.message, | |
| publicId: result.public_id, | |
| resourceType: result.resource_type, | |
| }) | |
| ); | |
| throw err; | |
| } | |
| } catch (err) { | |
| next(err); | |
| } finally { | |
| // Always clean up the local temp file, whether the upload succeeded, | |
| // failed partway, or Cloudinary itself errored out. | |
| if (tempFilePath) { | |
| fs.unlink(tempFilePath).catch((err) => | |
| logger.error("Failed to remove temp upload file", { error: err.message, tempFilePath }) | |
| ); | |
| } | |
| } | |
| } | |
| async function updateLesson(req, res, next) { | |
| try { | |
| const { title, description, isPreview, order } = req.body; | |
| const course = await Course.findById(req.params.id); | |
| if (!course) return res.status(404).json({ error: "Course not found." }); | |
| const section = course.curriculum.id(req.params.sectionId); | |
| const lesson = section?.lessons.id(req.params.lessonId); | |
| if (!section || !lesson) return res.status(404).json({ error: "Lesson not found." }); | |
| if (title !== undefined) lesson.title = title; | |
| if (description !== undefined) lesson.description = description; | |
| if (isPreview !== undefined) lesson.isPreview = isPreview === "true" || isPreview === true; | |
| if (order !== undefined) lesson.order = Number(order); | |
| await course.save(); | |
| res.json({ curriculum: course.toCurriculumJSON() }); | |
| } catch (err) { | |
| next(err); | |
| } | |
| } | |
| async function deleteLesson(req, res, next) { | |
| try { | |
| const course = await Course.findById(req.params.id); | |
| if (!course) return res.status(404).json({ error: "Course not found." }); | |
| const section = course.curriculum.id(req.params.sectionId); | |
| const lesson = section?.lessons.id(req.params.lessonId); | |
| if (!section || !lesson) return res.status(404).json({ error: "Lesson not found." }); | |
| await deleteAsset(lesson.cloudinaryPublicId, lesson.cloudinaryResourceType).catch((err) => | |
| logger.error("Failed to delete Cloudinary asset", { error: err.message }) | |
| ); | |
| lesson.deleteOne(); | |
| await course.save(); | |
| await AuditLog.create({ | |
| actor: req.user._id, | |
| actorEmail: req.user.email, | |
| action: "admin.lesson_deleted", | |
| targetType: "Course", | |
| targetId: course._id, | |
| ip: req.ip, | |
| userAgent: req.headers["user-agent"], | |
| }); | |
| res.json({ curriculum: course.toCurriculumJSON() }); | |
| } catch (err) { | |
| next(err); | |
| } | |
| } | |
| /** Admin editing view: full curriculum, same shape as the learner view (no raw Cloudinary IDs sent to the browser here either — uploads/deletes are the only mutating ops, playback still goes through the signed-URL endpoint). */ | |
| async function getCourseCurriculumForAdmin(req, res, next) { | |
| try { | |
| const course = await Course.findById(req.params.id); | |
| if (!course) return res.status(404).json({ error: "Course not found." }); | |
| res.json({ courseId: course._id, title: course.title, curriculum: course.toCurriculumJSON() }); | |
| } catch (err) { | |
| next(err); | |
| } | |
| } | |
| /** | |
| * Admin uploads/replaces a course's marketing thumbnail (shown on the | |
| * homepage "Our Academy" preview and the Academy catalogue page). If | |
| * the course already had one, the old Cloudinary asset is deleted so | |
| * replacing a thumbnail doesn't leave the previous image as an | |
| * orphaned file in storage. | |
| */ | |
| async function uploadThumbnail(req, res, next) { | |
| try { | |
| const course = await Course.findById(req.params.id); | |
| if (!course) return res.status(404).json({ error: "Course not found." }); | |
| if (!req.file) return res.status(400).json({ error: "No image file was uploaded." }); | |
| const previousPublicId = course.thumbnailPublicId; | |
| const result = await uploadThumbnailToCloudinary(req.file.path, { filenameHint: course.slug }); | |
| course.image = result.secure_url; | |
| course.thumbnailPublicId = result.public_id; | |
| await course.save(); | |
| if (previousPublicId) { | |
| deleteThumbnail(previousPublicId).catch((err) => | |
| logger.error("Failed to clean up replaced course thumbnail", { error: err.message, publicId: previousPublicId }) | |
| ); | |
| } | |
| await AuditLog.create({ | |
| actor: req.user._id, | |
| actorEmail: req.user.email, | |
| action: "admin.course_thumbnail_uploaded", | |
| targetType: "Course", | |
| targetId: course._id, | |
| ip: req.ip, | |
| userAgent: req.headers["user-agent"], | |
| }); | |
| res.json({ course }); | |
| } catch (err) { | |
| next(err); | |
| } finally { | |
| if (req.file) { | |
| fs.unlink(req.file.path).catch(() => {}); | |
| } | |
| } | |
| } | |
| module.exports = { | |
| getCourseCurriculum, | |
| getLessonAccess, | |
| getCourseCurriculumForAdmin, | |
| createSection, | |
| updateSection, | |
| deleteSection, | |
| uploadLesson, | |
| updateLesson, | |
| deleteLesson, | |
| uploadThumbnail, | |
| }; | |