Skip to content

Commit

Permalink
Added Video functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
Puneet-NJ committed Jan 7, 2025
1 parent 9a1ac3c commit 704e01a
Show file tree
Hide file tree
Showing 28 changed files with 1,413 additions and 42 deletions.
29 changes: 29 additions & 0 deletions backend/prisma/migrations/20250105194807_added/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
-- CreateTable
CREATE TABLE "CourseFolder" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"courseId" TEXT NOT NULL,

CONSTRAINT "CourseFolder_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "CourseContent" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"courseFolderId" TEXT NOT NULL,

CONSTRAINT "CourseContent_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "CourseFolder_courseId_name_key" ON "CourseFolder"("courseId", "name");

-- CreateIndex
CREATE UNIQUE INDEX "CourseContent_courseFolderId_name_key" ON "CourseContent"("courseFolderId", "name");

-- AddForeignKey
ALTER TABLE "CourseFolder" ADD CONSTRAINT "CourseFolder_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Courses"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "CourseContent" ADD CONSTRAINT "CourseContent_courseFolderId_fkey" FOREIGN KEY ("courseFolderId") REFERENCES "CourseFolder"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:
- Added the required column `isUploaded` to the `CourseContent` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "CourseContent" ADD COLUMN "isUploaded" BOOLEAN NOT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:
- Added the required column `contentUrl` to the `CourseContent` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "CourseContent" ADD COLUMN "contentUrl" TEXT NOT NULL;
27 changes: 27 additions & 0 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,33 @@ model Courses {
creator Course_Creator @relation(fields: [creatorId], references: [id])
purchases Purchases[]
courseFolders CourseFolder[]
}

model CourseFolder {
id String @id @default(uuid())
name String
courseId String
course Courses @relation(fields: [courseId], references: [id])
@@unique([courseId, name])
courseContents CourseContent[]
}

model CourseContent {
id String @id @default(uuid())
name String
contentUrl String
courseFolderId String
courseFolder CourseFolder @relation(fields: [courseFolderId], references: [id])
isUploaded Boolean
@@unique([courseFolderId, name])
}

model Purchases {
Expand Down
209 changes: 190 additions & 19 deletions backend/src/v1/routes/course.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { Router } from "express";
import { createCourseSchema, updateCourseSchema } from "../types/zod";
import {
createContentSchema,
createCourseSchema,
createFolderSchema,
updateCourseSchema,
} from "../types/zod";
import auth from "../middleware/auth";
import client from "../utils/prisma";
import { v4 as uuidv4 } from "uuid";
import { getPresignedUrl, deleteImageFromS3 } from "../utils/aws";
import { isCourseCreator } from "../utils/lib";

export const courseRouter = Router();

Expand All @@ -28,9 +34,9 @@ courseRouter.post("/", auth(["Admin"]), async (req, res) => {
return;
}

const courseThumbnailId = uuidv4();
const courseThumbnailId = `${name}/thumbnail`;

const signedUrl = await getPresignedUrl(courseThumbnailId);
const signedUrl = await getPresignedUrl(courseThumbnailId, "image/png");

const course = await client.courses.create({
data: {
Expand Down Expand Up @@ -63,12 +69,11 @@ courseRouter.post(
const courseId = req.params.courseId;
const adminId = res.locals.Admin.id;

try {
await client.courses.update({
where: { id: courseId, creatorId: adminId },
data: { isUploaded: true },
});
} catch (err) {
const course = await client.courses.update({
where: { id: courseId, creatorId: adminId },
data: { isUploaded: true },
});
if (!course) {
res.status(400).json({ msg: "You aren't the creator of the course" });
return;
}
Expand Down Expand Up @@ -107,13 +112,9 @@ courseRouter.put("/", auth(["Admin"]), async (req, res) => {
return;
}

const prevImageUrl = isUsersCourse.imageUrl.split(
process.env.CDN_LINK as string
);

const courseThumbnailId = uuidv4();
const courseThumbnailId = `${courseId}/thumbnail`;

const signedUrl = await getPresignedUrl(courseThumbnailId);
const signedUrl = await getPresignedUrl(courseThumbnailId, "image/png");

const response = await client.courses.update({
where: {
Expand All @@ -122,14 +123,10 @@ courseRouter.put("/", auth(["Admin"]), async (req, res) => {
data: {
description,
price,
imageUrl: `${process.env.CDN_LINK}/${courseThumbnailId}`,
isUploaded: false,
},
});

const prevImageKey = prevImageUrl[1].slice(1);
deleteImageFromS3(prevImageKey);

res.json({
msg: "Course info updated successfully",
courseId: response.id,
Expand Down Expand Up @@ -183,6 +180,7 @@ courseRouter.get("/:courseId", async (req, res) => {

const course = await client.courses.findFirst({
where: { id: courseId },
include: { courseFolders: { include: { courseContents: true } } },
});

if (!course) {
Expand All @@ -192,10 +190,41 @@ courseRouter.get("/:courseId", async (req, res) => {

res.json({ course });
} catch (err) {
console.log(err);

res.status(500).json({ msg: "Internal server error" });
}
});

courseRouter.get("/:courseId/:contentId", auth(["User"]), async (req, res) => {
try {
const { courseId, contentId } = req.params;
const userId = res.locals.User.id;

const purchased = await client.purchases.findFirst({
where: {
userId,
courseId,
},
});

if (!purchased) {
res.status(400).json({ msg: "You have not bought this course" });
return;
}

const content = await client.courseContent.findFirst({
where: { id: contentId },
});

res.json({ content });
} catch (err) {
console.log(err);

res.status(500).json({ msg: "Internal Server Error" });
}
});

courseRouter.get("/", async (req, res) => {
try {
const courses = await client.courses.findMany({});
Expand All @@ -207,3 +236,145 @@ courseRouter.get("/", async (req, res) => {
res.status(500).json({ msg: "Internal server error" });
}
});

courseRouter.post("/createFolder", auth(["Admin"]), async (req, res) => {
try {
const validateInput = createFolderSchema.safeParse(req.body);
if (!validateInput.success) {
res.status(411).json({ msg: "Invalid inputs" });
return;
}

const { courseId, name: folderName } = validateInput.data;
const creatorId = res.locals.Admin.id;

// check if the course is present
const isCreator = await isCourseCreator(courseId, creatorId);
if (isCreator.status !== 200) {
res.status(isCreator.status).json({ msg: isCreator.msg });
return;
}

// Course folder present
const folderPresent = await client.courseFolder.findFirst({
where: {
courseId,
name: folderName,
},
});
if (folderPresent) {
res.status(400).json({ msg: "Folder with similar name already exists" });
return;
}

const folder = await client.courseFolder.create({
data: {
name: folderName,
courseId: courseId,
},
});

res.json({ msg: "Folder created successfully" });
} catch (err) {
res.status(500).json({ msg: "Internal Server Error" });
}
});

courseRouter.post("/postContent", auth(["Admin"]), async (req, res) => {
try {
const validateInput = createContentSchema.safeParse(req.body);
if (!validateInput.success) {
res.status(411).json({ msg: "Invalid inputs" });
return;
}

const creatorId = res.locals.Admin.id;
const { folderId, name: contentName } = validateInput.data;

const folder = await client.courseFolder.findFirst({
where: {
id: folderId,
},
});
if (!folder) {
res.status(411).json({ msg: "Invalid folder ID" });
return;
}

const isCreator = await isCourseCreator(folder.courseId, creatorId);
if (isCreator.status !== 200) {
res.status(isCreator.status).json({ msg: isCreator.msg });
return;
}

const courseName = isCreator.course!.name.replace(/\s+/g, "");
const folderName = folder.name.replace(/\s+/g, "");

// already have content name

function sanitizeName(name: string) {
return name.replace(/[^a-zA-Z0-9-]/g, "-");
}

// Sanitize all parts
const sanitizedCourseName = sanitizeName(courseName);
const sanitizedFolderName = sanitizeName(folderName);
const sanitizedContentName = sanitizeName(contentName);

const objectKey = `${sanitizedCourseName}/${sanitizedFolderName}/${sanitizedContentName}`;

const encodedObjectKey = encodeURIComponent(objectKey).replace(/%2F/g, "/");

const signedUrl = await getPresignedUrl(encodedObjectKey, "video/mp4");

const content = await client.courseContent.create({
data: {
name: contentName,
courseFolderId: folderId,
isUploaded: false,
contentUrl: `${process.env.CDN_LINK}/${encodedObjectKey}`,
},
});

res.json({ signedUrl, contentId: content.id });
} catch (err) {
res.status(500).json({ msg: "Internal Server Error" });
}
});

courseRouter.post(
"/contentUploaded/:contentId",
auth(["Admin"]),
async (req, res) => {
try {
const creatorId = res.locals.Admin.id;
const contentId = req.params.contentId;

// need to check if the creator who is trying to update the courseContent is the actual creator of the course or not
const course = await client.courseContent.findFirst({
where: { id: contentId },
include: {
courseFolder: { include: { course: true } },
},
});

if (course?.courseFolder.course.creatorId !== creatorId) {
res.status(400).json({ msg: "You are not the creator of the course" });
return;
}

const content = await client.courseContent.update({
where: {
id: contentId,
},
data: {
isUploaded: true,
},
});

res.json({ msg: "Course updated successfully" });
} catch (err) {
res.status(500).json({ msg: "Internal Server Error" });
}
}
);
10 changes: 10 additions & 0 deletions backend/src/v1/types/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,13 @@ export const updateCourseSchema = z.object({
description: z.string(),
price: z.number(),
});

export const createFolderSchema = z.object({
courseId: z.string(),
name: z.string(),
});

export const createContentSchema = z.object({
folderId: z.string(),
name: z.string(),
});
8 changes: 4 additions & 4 deletions backend/src/v1/utils/aws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ const initilizeAws = () => {
return s3;
};

export const getPresignedUrl = (courseThumbnailId: string) => {
export const getPresignedUrl = (objectKey: string, contentType: string) => {
initilizeAws();

return s3.getSignedUrlPromise("putObject", {
Bucket: "puneet-course-app",
Key: courseThumbnailId,
Expires: 60,
ContentType: "image/png",
Key: objectKey,
Expires: 240,
ContentType: contentType,
});
};

Expand Down
Loading

0 comments on commit 704e01a

Please sign in to comment.