diff --git a/apps/docs/src/pages/en/self-hosting/self-host.md b/apps/docs/src/pages/en/self-hosting/self-host.md index 992552bc8..56a698614 100644 --- a/apps/docs/src/pages/en/self-hosting/self-host.md +++ b/apps/docs/src/pages/en/self-hosting/self-host.md @@ -84,100 +84,44 @@ docker compose up If you want to upload media (images, videos etc.) to your school, you need to configure [MediaLit](https://hub.docker.com/r/codelit/medialit). MediaLit powers CourseLit's media management and optimisation. MediaLit offers a Docker image which you can self host. -To self host, paste the following code in your `docker-compose.yml` file, under the existing content. +To self host, follow the following steps. -``` -medialit: - image: codelit/medialit - environment: - - DB_CONNECTION_STRING=${DB_CONNECTION_STRING_MEDIALIT} - - CLOUD_ENDPOINT=${CLOUD_ENDPOINT} - - CLOUD_REGION=${CLOUD_REGION} - - CLOUD_KEY=${CLOUD_KEY} - - CLOUD_SECRET=${CLOUD_SECRET} - - CLOUD_BUCKET_NAME=${CLOUD_BUCKET_NAME} - - CDN_ENDPOINT=${CDN_ENDPOINT} - - TEMP_FILE_DIR_FOR_UPLOADS=${TEMP_FILE_DIR_FOR_UPLOADS} - - PORT=8000 - - EMAIL_HOST=${EMAIL_HOST} - - EMAIL_USER=${EMAIL_USER} - - EMAIL_PASS=${EMAIL_PASS} - - EMAIL_FROM=${EMAIL_FROM} - - ENABLE_TRUST_PROXY=${ENABLE_TRUST_PROXY} - - CLOUD_PREFIX=${CLOUD_PREFIX} - ports: - - "8000:8000" - container_name: medialit - restart: on-failure -``` - -In your `.env` file, paste the following code (under the existing content) and change the values as per your environment. - -``` -# Medialit Server -DB_CONNECTION_STRING_MEDIALIT=mongodb_connection_string -CLOUD_ENDPOINT=aws_s3_endpoint -CLOUD_REGION=aws_s3_region -CLOUD_KEY=aws_s3_key -CLOUD_SECRET=aws_s3_secret -CLOUD_BUCKET_NAME=aws_s3_bucket_name -CDN_ENDPOINT=aws_s3_cdn_endpoint -TEMP_FILE_DIR_FOR_UPLOADS=path_to_directory -PORT=8000 -CLOUD_PREFIX=medialit -``` - -Restart the services by running the following commands. - -``` -docker compose stop -docker compose up -``` - -> **NOTE**: The MediaLit installation is done but is not yet integrated with CourseLit! There are a few more steps. Keep reading. - -#### Obtain the API key from MediaLit - -First you need to obtain the container id of your MediaLit instance. To do this, run: - -``` -docker ps -``` - -Once you have the ID of the `MediaLit` container, run the following to generate an API key - -``` -docker exec node /app/apps/api/dist/src/scripts/create-local-user.js -``` - -Keep the generated API key safe. We will use it in the following step. - -> For the most up-to-date instructions, refer to the official [Readme](https://github.com/codelitdev/medialit?tab=readme-ov-file#creating-a-local-user) of MediaLit. +1. Uncomment the block under the `app` service in `docker-compose.yml` which says the following. -#### Using Self-hosted MediaLit With CourseLit + ``` + # - MEDIALIT_APIKEY=${MEDIALIT_APIKEY} + # - MEDIALIT_SERVER=http://host.docker.internal:8000 + ``` -Open the `.env` file and add the following lines. +2. Uncomment the block titled `MediaLit` in `docker-compose.yml`. -``` -MEDIALIT_SERVER=http://localhost:8000 -MEDIALIT_APIKEY=key_from_above_step -``` +3. In your `.env` file, paste the following code (under the existing content) and change the values as per your environment. -Now, in the `docker-compose.yml` file, add the following two lines under the `environment` block of the `app` service. + ``` + # Medialit Server + CLOUD_ENDPOINT=aws_s3_endpoint + CLOUD_REGION=aws_s3_region + CLOUD_KEY=aws_s3_key + CLOUD_SECRET=aws_s3_secret + CLOUD_BUCKET_NAME=aws_s3_bucket_name + S3_ENDPOINT=aws_s3_cdn_endpoint + CLOUD_PREFIX=medialit + MEDIALIT_APIKEY=key_to_be_obtained_docker_compose_logs + ``` -``` - - MEDIALIT_APIKEY=${MEDIALIT_APIKEY} - - MEDIALIT_SERVER=${MEDIALIT_SERVER} -``` +4. Restart the services once to generate a user and an API key in MediaLit database. The API key + will be printed to the docker compose logs. The relevant logs will look something like the following. + `sh + @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + @ API key: testcktI8Sa71QUgYtest @ + @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + ` -Restart the server by running the following commands. + Copy the API key. -``` -docker compose stop -docker compose up -``` +5. Update the `MEDIALIT_APIKEY` value in `.env` file and restart the service once again. -That's it! You now have a fully functioning LMS powered by CourseLit. +6. That's it! You now have a fully functioning LMS powered by CourseLit and MediaLit. ## Hosted version diff --git a/apps/web/components/community/index.tsx b/apps/web/components/community/index.tsx index 080bddfb8..fe44df16f 100644 --- a/apps/web/components/community/index.tsx +++ b/apps/web/components/community/index.tsx @@ -35,6 +35,7 @@ import { CommunityMedia, CommunityPost, Constants, + Media, } from "@courselit/common-models"; import LoadingSkeleton from "./loading-skeleton"; import { formattedLocaleDate, hasCommunityPermission } from "@ui-lib/utils"; @@ -458,94 +459,93 @@ export function CommunityForum({ return response.url; }; - const removeFile = async (mediaId: string) => { - try { - const fetch = new FetchBuilder() - .setUrl(`${address.backend}/api/media/${mediaId}`) - .setHttpMethod("DELETE") - .setIsGraphQLEndpoint(false) - .build(); - const response = await fetch.exec(); - if (response.message !== "success") { - throw new Error(response.message); - } - } catch (err: any) { - console.error("Error in removing file", err.message); - } - }; + // const removeFile = async (mediaId: string) => { + // try { + // const fetch = new FetchBuilder() + // .setUrl(`${address.backend}/api/media/${mediaId}`) + // .setHttpMethod("DELETE") + // .setIsGraphQLEndpoint(false) + // .build(); + // const response = await fetch.exec(); + // if (response.message !== "success") { + // throw new Error(response.message); + // } + // } catch (err: any) { + // console.error("Error in removing file", err.message); + // } + // }; const createPost = async ( newPost: Pick & { media: MediaItem[]; }, ) => { - if (newPost.media.length > 0) { - newPost.media = await uploadAttachments(newPost.media); - } - const mutation = ` - mutation ($id: String!, $title: String!, $content: String!, $category: String!, $media: [CommunityPostInputMedia]) { - post: createCommunityPost( - id: $id, - title: $title, - content: $content, - category: $category, - media: $media - ) { - communityId - postId - title - content - category - media { - type + try { + if (newPost.media.length > 0) { + newPost.media = await uploadAttachments(newPost.media); + } + const mutation = ` + mutation ($id: String!, $title: String!, $content: String!, $category: String!, $media: [CommunityPostInputMedia]) { + post: createCommunityPost( + id: $id, + title: $title, + content: $content, + category: $category, + media: $media + ) { + communityId + postId title - url + content + category media { - mediaId - file - thumbnail - originalFileName + type + title + url + media { + mediaId + file + thumbnail + originalFileName + } } - } - likesCount - commentsCount - updatedAt - hasLiked - user { - userId - name - avatar { - mediaId - file - thumbnail + likesCount + commentsCount + updatedAt + hasLiked + user { + userId + name + avatar { + mediaId + file + thumbnail + } } + pinned } - pinned } - } - `; - - const fetch = new FetchBuilder() - .setUrl(`${address.backend}/api/graph`) - .setPayload({ - query: mutation, - variables: { - id: community?.communityId, - content: newPost.content, - category: newPost.category, - title: newPost.title, - media: newPost.media.map((m) => ({ - type: m.type, - title: m.title, - url: m.url, - media: m.media, - })), - }, - }) - .setIsGraphQLEndpoint(true) - .build(); + `; - try { + const fetch = new FetchBuilder() + .setUrl(`${address.backend}/api/graph`) + .setPayload({ + query: mutation, + variables: { + id: community?.communityId, + content: newPost.content, + category: newPost.category, + title: newPost.title, + media: newPost.media.map((m) => ({ + type: m.type, + title: m.title, + url: m.url, + media: m.media, + })), + }, + }) + .setIsGraphQLEndpoint(true) + .build(); const response = await fetch.exec(); if (response.post) { setPosts((prevPosts) => [response.post, ...prevPosts]); @@ -557,8 +557,9 @@ export function CommunityForum({ } } catch (err: any) { toast({ - title: "Error", + title: TOAST_TITLE_ERROR, description: err.message, + variant: "destructive", }); } }; @@ -580,8 +581,8 @@ export function CommunityForum({ const presignedUrl = await getPresignedUrl(); const media = await uploadToServer(presignedUrl, file); return media; - } catch (err: any) { - console.error(err.message); + } catch (err) { + throw new Error(`Media upload: ${err.message}`); } }; @@ -905,7 +906,7 @@ export function CommunityForum({ setReportReason(""); } catch (err: any) { toast({ - title: "Error", + title: TOAST_TITLE_ERROR, description: err.message, variant: "destructive", }); diff --git a/apps/web/pages/api/media/presigned.ts b/apps/web/pages/api/media/presigned.ts index ee260a2fd..b51bc3f52 100644 --- a/apps/web/pages/api/media/presigned.ts +++ b/apps/web/pages/api/media/presigned.ts @@ -50,7 +50,7 @@ export default async function handler( ); return res.status(200).json({ url: response }); } catch (err: any) { - error(err.mssage, { + error(err.message, { stack: err.stack, }); return res.status(500).json({ error: err.message }); diff --git a/deployment/docker/docker-compose.yml b/deployment/docker/docker-compose.yml index de8696eec..b56b328ee 100644 --- a/deployment/docker/docker-compose.yml +++ b/deployment/docker/docker-compose.yml @@ -21,7 +21,7 @@ services: # In production, replace the following with the connection string of a cloud # hosted instance of MongoDB. - - DB_CONNECTION_STRING=mongodb://root:example@mongo + - DB_CONNECTION_STRING=mongodb://root:example@mongo/courselit?authSource=admin # CourseLit uses Magic links to provide login functionality. Hence, it requires # access to the mail sending facility. @@ -46,8 +46,17 @@ services: # CourseLit uses MediaLit (our another open-source software) to manage files on # a AWS S3 compatible storage. MediaLit is available as a hosted service # at https://medialit.cloud. You can self host it as well. + # + # Uncomment the following lines to use MediaLit as a hosted service. + # The MEDIALIT_APIKEY can be obtained by signing up at https://medialit.cloud. # - MEDIALIT_APIKEY=${MEDIALIT_APIKEY} # - MEDIALIT_SERVER=https://api.medialit.cloud + # + # Uncomment the following lines to use MediaLit as a self hosted service. + # The MEDIALIT_APIKEY can be obtained by running the medialit service locally and + # checking the logs for the API key. + # - MEDIALIT_APIKEY=${MEDIALIT_APIKEY} + # - MEDIALIT_SERVER=http://host.docker.internal:8000 expose: - "${PORT:-80}" @@ -110,3 +119,42 @@ services: # expose: # - "6379" # restart: on-failure + + # MediaLit is a hosted service for file storage. Uncomment the following block to + # self host it. You will need a S3 compatible storage to use it. + # Uncomment the following block to use MediaLit as a self-hosted service. + # + # medialit: + # image: codelit/medialit + # environment: + # - EMAIL=${SUPER_ADMIN_EMAIL?'SUPER_ADMIN_EMAIL is required to set up a user on MediaLit'} + + # # In production, replace the following with the connection string of a cloud + # # hosted instance of MongoDB. + # - DB_CONNECTION_STRING=mongodb://root:example@mongo/medialit?authSource=admin + + # # AWS S3 compatible storage configuration + # - CLOUD_ENDPOINT=${CLOUD_ENDPOINT?'CLOUD_ENDPOINT is required'} + # - CLOUD_REGION=${CLOUD_REGION?'CLOUD_REGION is required'} + # - CLOUD_KEY=${CLOUD_KEY?'CLOUD_KEY is required'} + # - CLOUD_SECRET=${CLOUD_SECRET?'CLOUD_SECRET is required'} + # - CLOUD_BUCKET_NAME=${CLOUD_BUCKET_NAME?'CLOUD_BUCKET_NAME is required'} + # - CLOUD_PREFIX=${CLOUD_PREFIX?'CLOUD_PREFIX is required'} + # - S3_ENDPOINT=${S3_ENDPOINT?'S3_ENDPOINT is required'} + + # # Temporary file directory for uploads transformations + # - TEMP_FILE_DIR_FOR_UPLOADS=/tmp + + # - ENABLE_TRUST_PROXY=${ENABLE_TRUST_PROXY} + + # # CloudFront configuration + # - USE_CLOUDFRONT=${USE_CLOUDFRONT} + # - CLOUDFRONT_ENDPOINT=${CLOUDFRONT_ENDPOINT} + # - CLOUDFRONT_KEY_PAIR_ID=${CLOUDFRONT_KEY_PAIR_ID} + # - CLOUDFRONT_PRIVATE_KEY=${CLOUDFRONT_PRIVATE_KEY} + # - CDN_MAX_AGE=${CDN_MAX_AGE} + # ports: + # - "8000:80" + # depends_on: + # - mongo + # restart: on-failure \ No newline at end of file diff --git a/packages/components-library/src/media-selector/index.tsx b/packages/components-library/src/media-selector/index.tsx index aed9ef765..91b1ec69e 100644 --- a/packages/components-library/src/media-selector/index.tsx +++ b/packages/components-library/src/media-selector/index.tsx @@ -9,7 +9,7 @@ import { FetchBuilder } from "@courselit/utils"; import Form from "../form"; import FormField from "../form-field"; import React from "react"; -import { Button2, PageBuilderPropertyHeader, Tooltip } from ".."; +import { Button2, PageBuilderPropertyHeader, Tooltip, useToast } from ".."; import { X } from "lucide-react"; interface Strings { @@ -73,6 +73,7 @@ const MediaSelector = (props: MediaSelectorProps) => { const fileInput: React.RefObject = React.createRef(); const [selectedFile, setSelectedFile] = useState(); const [caption, setCaption] = useState(""); + const { toast } = useToast(); const { strings, address, @@ -81,7 +82,13 @@ const MediaSelector = (props: MediaSelectorProps) => { srcTitle, tooltip, disabled = false, - onError, + onError = (err: Error) => { + toast({ + title: "Error", + description: `Media upload: ${err.message}`, + variant: "destructive", + }); + }, } = props; const onSelection = (media: Media) => { @@ -146,7 +153,7 @@ const MediaSelector = (props: MediaSelectorProps) => { const media = await uploadToServer(presignedUrl); onSelection(media); } catch (err: any) { - onError && onError(err); + onError(err); } finally { setUploading(false); setSelectedFile(undefined); @@ -173,7 +180,7 @@ const MediaSelector = (props: MediaSelectorProps) => { props.onRemove(); } } catch (err: any) { - onError && onError(err); + onError(err); } finally { setUploading(false); setDialogOpened(false);