@@ -287,7 +280,7 @@ const UsersManager = ({ address, dispatch, loading }: UserManagerProps) => {
src={
user.avatar
? user.avatar?.file
- : "/favicon.ico"
+ : "/courselit_backdrop_square.webp"
}
/>
diff --git a/apps/web/components/admin/users/permissions-to-caption-map.ts b/apps/web/components/admin/users/permissions-to-caption-map.ts
index 5f50fb375..d52028ffc 100644
--- a/apps/web/components/admin/users/permissions-to-caption-map.ts
+++ b/apps/web/components/admin/users/permissions-to-caption-map.ts
@@ -6,9 +6,6 @@ import {
PERM_COURSE_PUBLISH,
PERM_ENROLL_IN_COURSE,
PERM_MEDIA_MANAGE,
- PERM_MEDIA_MANAGE_ANY,
- PERM_MEDIA_VIEW_ANY,
- PERM_MEDIA_UPLOAD,
PERM_SETTINGS,
PERM_USERS,
PERM_SITE,
@@ -19,10 +16,7 @@ const permissionToCaptionMap = {
[permissions.manageAnyCourse]: PERM_COURSE_MANAGE_ANY,
[permissions.publishCourse]: PERM_COURSE_PUBLISH,
[permissions.enrollInCourse]: PERM_ENROLL_IN_COURSE,
- [permissions.viewAnyMedia]: PERM_MEDIA_VIEW_ANY,
- [permissions.uploadMedia]: PERM_MEDIA_UPLOAD,
[permissions.manageMedia]: PERM_MEDIA_MANAGE,
- [permissions.manageAnyMedia]: PERM_MEDIA_MANAGE_ANY,
[permissions.manageSite]: PERM_SITE,
[permissions.manageSettings]: PERM_SETTINGS,
[permissions.manageUsers]: PERM_USERS,
diff --git a/apps/web/components/public/article.tsx b/apps/web/components/public/article.tsx
index 7146f7d71..9e93e7331 100644
--- a/apps/web/components/public/article.tsx
+++ b/apps/web/components/public/article.tsx
@@ -46,7 +46,7 @@ const Article = (props: ArticleProps) => {
src={
profile.avatar
? profile.avatar?.file
- : "/favicon.ico"
+ : "/courselit_backdrop_square.webp"
}
/>
diff --git a/apps/web/components/public/base-layout/branding.tsx b/apps/web/components/public/base-layout/branding.tsx
index 4acd2a2c0..8642be10d 100644
--- a/apps/web/components/public/base-layout/branding.tsx
+++ b/apps/web/components/public/base-layout/branding.tsx
@@ -15,9 +15,9 @@ const Branding = ({ siteinfo }: BrandingProps) => {
diff --git a/apps/web/components/public/base-layout/index.tsx b/apps/web/components/public/base-layout/index.tsx
index 70e377ff3..7c7506d4a 100644
--- a/apps/web/components/public/base-layout/index.tsx
+++ b/apps/web/components/public/base-layout/index.tsx
@@ -27,7 +27,7 @@ interface MasterLayoutProps {
theme: Theme;
dispatch: AppDispatch;
description?: string;
- socialImage: Media;
+ socialImage?: Media;
robotsAllowed?: boolean;
}
diff --git a/apps/web/config/strings.ts b/apps/web/config/strings.ts
index d8c321976..545a2aec0 100644
--- a/apps/web/config/strings.ts
+++ b/apps/web/config/strings.ts
@@ -20,7 +20,7 @@ export const responses = {
user_not_found: "User not found.",
request_not_authenticated: "Request not authenticated",
content_cannot_be_null: "Content cannot be empty",
- media_id_cannot_be_null: "Media Id cannot be empty",
+ media_id_cannot_be_null: "Media cannot be empty",
item_not_found: "Item not found",
drip_not_released: "This section is not yet released for you",
not_a_creator: "You do not have rights to perform this action",
diff --git a/apps/web/graphql/lessons/helpers.ts b/apps/web/graphql/lessons/helpers.ts
index a114771e5..7717fb40a 100644
--- a/apps/web/graphql/lessons/helpers.ts
+++ b/apps/web/graphql/lessons/helpers.ts
@@ -14,10 +14,10 @@ type LessonValidatorProps = Pick<
export const lessonValidator = (lessonData: LessonValidatorProps) => {
validateTextContent(lessonData);
- validateMediaContent(lessonData);
+ // validateMediaContent(lessonData);
};
-function validateTextContent(lessonData: LessonValidatorProps) {
+export function validateTextContent(lessonData: LessonValidatorProps) {
const content = lessonData.content ? JSON.parse(lessonData.content) : null;
if ([text, embed].includes(lessonData.type)) {
diff --git a/apps/web/graphql/lessons/types.ts b/apps/web/graphql/lessons/types.ts
index a73593077..342885816 100644
--- a/apps/web/graphql/lessons/types.ts
+++ b/apps/web/graphql/lessons/types.ts
@@ -93,7 +93,7 @@ const lessonInputType = new GraphQLInputObjectType({
type: new GraphQLNonNull(GraphQLBoolean),
},
content: { type: GraphQLString },
- media: { type: mediaTypes.mediaInputType },
+ // media: { type: mediaTypes.mediaInputType },
downloadable: { type: GraphQLBoolean },
groupId: { type: new GraphQLNonNull(GraphQLID) },
},
diff --git a/apps/web/graphql/pages/logic.ts b/apps/web/graphql/pages/logic.ts
index db3a49bec..100dce86d 100644
--- a/apps/web/graphql/pages/logic.ts
+++ b/apps/web/graphql/pages/logic.ts
@@ -237,7 +237,7 @@ export const updatePage = async ({
if (description) {
page.draftDescription = description;
}
- if (socialImage) {
+ if (typeof socialImage !== "undefined") {
page.draftSocialImage = socialImage;
}
if (typeof robotsAllowed === "boolean") {
@@ -286,14 +286,12 @@ export const publish = async (
page.description = page.draftDescription;
page.draftDescription = undefined;
}
- if (page.draftSocialImage) {
- page.socialImage = page.draftSocialImage;
- page.draftSocialImage = undefined;
- }
if (page.draftRobotsAllowed) {
page.robotsAllowed = page.draftRobotsAllowed;
page.draftRobotsAllowed = undefined;
}
+ page.socialImage = page.draftSocialImage;
+
ctx.subdomain.typefaces = ctx.subdomain.draftTypefaces;
ctx.subdomain.sharedWidgets = ctx.subdomain.draftSharedWidgets;
ctx.subdomain.draftSharedWidgets = {};
diff --git a/apps/web/graphql/users/logic.ts b/apps/web/graphql/users/logic.ts
index e7543c221..b30389a53 100644
--- a/apps/web/graphql/users/logic.ts
+++ b/apps/web/graphql/users/logic.ts
@@ -105,7 +105,7 @@ export const updateUser = async (userData: any, ctx: GQLContext) => {
permissions.manageUsers,
]);
if (!hasPermissionToManageUser) {
- if (id !== ctx.user._id) {
+ if (id !== ctx.user._id.toString()) {
throw new Error(responses.action_not_allowed);
}
}
@@ -118,9 +118,12 @@ export const updateUser = async (userData: any, ctx: GQLContext) => {
continue;
}
- if (!["subscribedToUpdates"].includes(key) && id === ctx.user._id) {
- throw new Error(responses.action_not_allowed);
- }
+ // if (
+ // !["subscribedToUpdates"].includes(key) &&
+ // id === ctx.user._id.toString()
+ // ) {
+ // throw new Error(responses.action_not_allowed);
+ // }
if (key === "tags") {
addTags(userData["tags"], ctx);
@@ -282,17 +285,16 @@ export async function createUser({
constants.permissions.manageCourse,
constants.permissions.manageAnyCourse,
constants.permissions.publishCourse,
- // TODO: replace media perms with course perms
constants.permissions.manageMedia,
- constants.permissions.manageAnyMedia,
- constants.permissions.uploadMedia,
- constants.permissions.viewAnyMedia,
constants.permissions.manageSite,
constants.permissions.manageSettings,
constants.permissions.manageUsers,
];
} else {
- newUser.permissions = [constants.permissions.enrollInCourse];
+ newUser.permissions = [
+ constants.permissions.enrollInCourse,
+ constants.permissions.manageMedia,
+ ];
}
newUser.lead = lead;
const user = await UserModel.create(newUser);
diff --git a/apps/web/models/Media.ts b/apps/web/models/Media.ts
index 3efdc98a1..c051072fb 100644
--- a/apps/web/models/Media.ts
+++ b/apps/web/models/Media.ts
@@ -3,7 +3,9 @@ import mongoose from "mongoose";
import constants from "../config/constants";
const { publicMedia, privateMedia } = constants;
-const MediaSchema = new mongoose.Schema({
+type MediaWithOwner = Media & { userId: string };
+
+const MediaSchema = new mongoose.Schema({
mediaId: { type: String, required: true },
originalFileName: { type: String, required: true },
mimeType: { type: String, required: true },
diff --git a/apps/web/pages/api/media/[mediaId].ts b/apps/web/pages/api/media/[mediaId].ts
deleted file mode 100644
index 7908ae0af..000000000
--- a/apps/web/pages/api/media/[mediaId].ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { NextApiRequest, NextApiResponse } from "next";
-import { responses } from "../../../config/strings";
-import * as medialitService from "../../../services/medialit";
-import { UIConstants as constants } from "@courselit/common-models";
-import { checkPermission } from "@courselit/utils";
-import User from "@models/User";
-import DomainModel, { Domain } from "@models/Domain";
-import { auth } from "@/auth";
-
-export default async function handler(
- req: NextApiRequest,
- res: NextApiResponse,
-) {
- if (req.method !== "DELETE") {
- return res.status(405).json({ message: "Not allowed" });
- }
-
- const domain = await DomainModel.findOne({
- name: req.headers.domain,
- });
- if (!domain) {
- return res.status(404).json({ message: "Domain not found" });
- }
-
- const session = await auth(req, res);
-
- let user;
- if (session) {
- user = await User.findOne({
- email: session.user!.email,
- domain: domain._id,
- active: true,
- });
- }
-
- if (!user) {
- return res.status(401).json({});
- }
-
- if (
- !checkPermission(user!.permissions, [
- constants.permissions.manageAnyCourse,
- constants.permissions.manageCourse,
- ])
- ) {
- throw new Error(responses.action_not_allowed);
- }
-
- const { mediaId } = req.query;
- try {
- let response = await medialitService.deleteMedia(mediaId);
- return res.status(200).json({ message: responses.success });
- } catch (err: any) {
- return res.status(500).json({ error: responses.internal_error });
- }
-}
diff --git a/apps/web/pages/api/media/[mediaId]/[type].ts b/apps/web/pages/api/media/[mediaId]/[type].ts
new file mode 100644
index 000000000..8bcdae174
--- /dev/null
+++ b/apps/web/pages/api/media/[mediaId]/[type].ts
@@ -0,0 +1,160 @@
+import { NextApiRequest, NextApiResponse } from "next";
+import { responses } from "@/config/strings";
+import * as medialitService from "@/services/medialit";
+import { UIConstants as constants } from "@courselit/common-models";
+import { checkPermission } from "@courselit/utils";
+import UserModel, { User } from "@models/User";
+import DomainModel, { Domain } from "@models/Domain";
+import { auth } from "@/auth";
+import CourseModel, { Course } from "@models/Course";
+import LessonModel, { Lesson } from "@models/Lesson";
+import PageModel, { Page } from "@models/Page";
+
+const types = ["course", "lesson", "page", "user", "domain"] as const;
+
+type MediaType = (typeof types)[number];
+
+export default async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse,
+) {
+ if (req.method !== "DELETE") {
+ return res.status(405).json({ message: "Not allowed" });
+ }
+
+ const domain = await DomainModel.findOne({
+ name: req.headers.domain,
+ });
+ if (!domain) {
+ return res.status(404).json({ message: "Domain not found" });
+ }
+
+ const session = await auth(req, res);
+
+ let user;
+ if (session) {
+ user = await UserModel.findOne({
+ email: session.user!.email,
+ domain: domain._id,
+ active: true,
+ });
+ }
+
+ if (!user) {
+ return res.status(401).json({});
+ }
+
+ const { mediaId, type } = req.query;
+ if (!types.includes(type as MediaType)) {
+ return res.status(400).json({ message: "Bad request" });
+ }
+
+ if (
+ !(await isActionAllowed(user, type as any, mediaId as string, domain))
+ ) {
+ ("");
+ return res.status(403).json({ message: responses.action_not_allowed });
+ }
+
+ try {
+ let response = await medialitService.deleteMedia(mediaId);
+ return res.status(200).json({ message: responses.success });
+ } catch (err: any) {
+ return res.status(500).json({ error: responses.internal_error });
+ }
+}
+
+async function isActionAllowed(
+ user: User,
+ type: MediaType,
+ mediaId: string,
+ domain: Domain,
+) {
+ if (
+ !checkPermission(user.permissions, [constants.permissions.manageMedia])
+ ) {
+ return false;
+ }
+
+ switch (type) {
+ case "course":
+ const course = await CourseModel.findOne({
+ domain: domain._id,
+ "featuredImage.mediaId": mediaId,
+ });
+ if (!course) {
+ return false;
+ }
+ if (
+ checkPermission(user.permissions, [
+ constants.permissions.manageAnyCourse,
+ ])
+ ) {
+ return true;
+ } else {
+ return (
+ course.creatorId === user.userId &&
+ checkPermission(user.permissions, [
+ constants.permissions.manageCourse,
+ ])
+ );
+ }
+ case "lesson":
+ const lesson = await LessonModel.findOne({
+ domain: domain._id,
+ "media.mediaId": mediaId,
+ });
+ if (!lesson) {
+ return false;
+ }
+ if (
+ checkPermission(user.permissions, [
+ constants.permissions.manageAnyCourse,
+ ])
+ ) {
+ return true;
+ } else {
+ return (
+ lesson?.creatorId.toString() === user._id.toString() &&
+ checkPermission(user.permissions, [
+ constants.permissions.manageCourse,
+ ])
+ );
+ }
+ case "page":
+ const pages = await PageModel.find({
+ domain: domain._id,
+ });
+ let mediaBelongsToThisDomain = false;
+ for (const p of pages) {
+ const fullContent =
+ JSON.stringify(p.layout) +
+ JSON.stringify(p.draftLayout) +
+ JSON.stringify(p.socialImage) +
+ JSON.stringify(p.draftSocialImage);
+ if (fullContent.indexOf(`"mediaId":"${mediaId}"`) !== -1) {
+ mediaBelongsToThisDomain = true;
+ break;
+ }
+ }
+ return (
+ mediaBelongsToThisDomain &&
+ checkPermission(user.permissions, [
+ constants.permissions.manageSite,
+ ])
+ );
+ case "user":
+ return (
+ user.avatar.mediaId === mediaId ||
+ checkPermission(user.permissions, [
+ constants.permissions.manageUsers,
+ ])
+ );
+ case "domain":
+ return checkPermission(user.permissions, [
+ constants.permissions.manageSettings,
+ ]);
+ default:
+ return false;
+ }
+}
diff --git a/apps/web/pages/api/media/presigned.ts b/apps/web/pages/api/media/presigned.ts
index 6fe1c43c9..ee260a2fd 100644
--- a/apps/web/pages/api/media/presigned.ts
+++ b/apps/web/pages/api/media/presigned.ts
@@ -6,6 +6,7 @@ import { checkPermission } from "@courselit/utils";
import User from "@models/User";
import DomainModel, { Domain } from "@models/Domain";
import { auth } from "@/auth";
+import { error } from "@/services/logger";
export default async function handler(
req: NextApiRequest,
@@ -38,16 +39,20 @@ export default async function handler(
}
if (
- !checkPermission(user!.permissions, [constants.permissions.uploadMedia])
+ !checkPermission(user!.permissions, [constants.permissions.manageMedia])
) {
- throw new Error(responses.action_not_allowed);
+ return res.status(403).json({ message: responses.action_not_allowed });
}
+
try {
let response = await medialitService.getPresignedUrlForUpload(
domain.name,
);
return res.status(200).json({ url: response });
} catch (err: any) {
+ error(err.mssage, {
+ stack: err.stack,
+ });
return res.status(500).json({ error: err.message });
}
}
diff --git a/apps/web/pages/dashboard/product/[id]/section/[section]/lesson/new.tsx b/apps/web/pages/dashboard/product/[id]/section/[section]/lesson/new.tsx
index 8ca6ebf8d..938efe0db 100644
--- a/apps/web/pages/dashboard/product/[id]/section/[section]/lesson/new.tsx
+++ b/apps/web/pages/dashboard/product/[id]/section/[section]/lesson/new.tsx
@@ -1,15 +1,10 @@
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
-import { BUTTON_NEW_LESSON_TEXT } from "../../../../../../../ui-config/strings";
+import { BUTTON_NEW_LESSON_TEXT } from "@/ui-config/strings";
-const BaseLayout = dynamic(
- () => import("../../../../../../../components/admin/base-layout"),
-);
+const BaseLayout = dynamic(() => import("@/components/admin/base-layout"));
const LessonEditor = dynamic(
- () =>
- import(
- "../../../../../../../components/admin/products/editor/content/lesson"
- ),
+ () => import("@/components/admin/products/editor/content/lesson"),
);
function NewLesson({}) {
diff --git a/apps/web/pages/login.tsx b/apps/web/pages/login.tsx
index 5df3e2f4c..9a7c35566 100644
--- a/apps/web/pages/login.tsx
+++ b/apps/web/pages/login.tsx
@@ -147,7 +147,8 @@ const Login = ({ page, auth, dispatch }: LoginProps) => {
{showCode && (
- {BTN_LOGIN_CODE_INTIMATION} {email}
+ {BTN_LOGIN_CODE_INTIMATION}{" "}
+ {email}
|