diff --git a/.gitignore b/.gitignore index 3eeb18b3..d3de4f6f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ - # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -10,8 +9,11 @@ __pycache__/ # Visual Studio Code .vscode/ -# Learnhouse -content/* +# Learnhouse +content/org_* + +# Flyio +fly.toml # Distribution / packaging .Python @@ -88,7 +90,7 @@ target/ profile_default/ ipython_config.py -# ruff +# ruff .ruff/ # pyenv @@ -166,4 +168,4 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ \ No newline at end of file +#.idea/ diff --git a/Dockerfile b/Dockerfile index 577b83f3..f42ea0a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,4 +14,4 @@ RUN pip install --no-cache-dir --upgrade -r /usr/learnhouse/requirements.txt COPY ./ /usr/learnhouse # -CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80" , "--reload"] +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80" ] diff --git a/app.py b/app.py index 9dce8265..0baf3d42 100644 --- a/app.py +++ b/app.py @@ -25,48 +25,44 @@ app = FastAPI( title=learnhouse_config.site_name, description=learnhouse_config.site_description, version="0.1.0", - root_path="/" + root_path="/", ) app.add_middleware( CORSMiddleware, allow_origin_regex=learnhouse_config.hosting_config.allowed_regexp, - allow_origins=learnhouse_config.hosting_config.allowed_origins, allow_methods=["*"], allow_credentials=True, - allow_headers=["*"] + allow_headers=["*"], ) # Gzip Middleware (will add brotli later) app.add_middleware(GZipMiddleware, minimum_size=1000) -# Static Files -app.mount("/content", StaticFiles(directory="content"), name="content") - - # Events app.add_event_handler("startup", startup_app(app)) app.add_event_handler("shutdown", shutdown_app(app)) # JWT Exception Handler -@ app.exception_handler(AuthJWTException) +@app.exception_handler(AuthJWTException) def authjwt_exception_handler(request: Request, exc: AuthJWTException): return JSONResponse( status_code=exc.status_code, # type: ignore - content={"detail": exc.message} # type: ignore + content={"detail": exc.message}, # type: ignore ) +# Static Files +app.mount("/content", StaticFiles(directory="content"), name="content") + # Global Routes app.include_router(v1_router) # General Routes -@ app.get("/") +@app.get("/") async def root(): return {"Message": "Welcome to LearnHouse ✨"} - - diff --git a/config/config.py b/config/config.py index 024728f3..93c3fa73 100644 --- a/config/config.py +++ b/config/config.py @@ -16,6 +16,7 @@ class CookieConfig(BaseModel): class GeneralConfig(BaseModel): development_mode: bool + install_mode: bool class SecurityConfig(BaseModel): @@ -71,6 +72,10 @@ def get_learnhouse_config() -> LearnHouseConfig: development_mode = env_development_mode or yaml_config.get("general", {}).get( "development_mode" ) + env_install_mode = os.environ.get("LEARNHOUSE_INSTALL_MODE") + install_mode = env_install_mode or yaml_config.get("general", {}).get( + "install_mode" + ) # Security Config env_auth_jwt_secret_key = os.environ.get("LEARNHOUSE_AUTH_JWT_SECRET_KEY") @@ -128,9 +133,8 @@ def get_learnhouse_config() -> LearnHouseConfig: cookie_config = CookieConfig(domain=cookies_domain) env_content_delivery_type = os.environ.get("LEARNHOUSE_CONTENT_DELIVERY_TYPE") - content_delivery_type: str = ( + content_delivery_type: str = env_content_delivery_type or ( (yaml_config.get("hosting_config", {}).get("content_delivery", {}).get("type")) - or env_content_delivery_type or "filesystem" ) # default to filesystem @@ -207,7 +211,9 @@ def get_learnhouse_config() -> LearnHouseConfig: site_name=site_name, site_description=site_description, contact_email=contact_email, - general_config=GeneralConfig(development_mode=bool(development_mode)), + general_config=GeneralConfig( + development_mode=bool(development_mode), install_mode=bool(install_mode) + ), hosting_config=hosting_config, database_config=database_config, security_config=SecurityConfig(auth_jwt_secret_key=auth_jwt_secret_key), diff --git a/config/config.yaml b/config/config.yaml index bf151243..12ee9e60 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -3,7 +3,8 @@ site_description: LearnHouse is an open-source platform tailored for learning ex contact_email: hi@learnhouse.app general: - development_mode: true + development_mode: false + install_mode: false security: auth_jwt_secret_key: secret diff --git a/content/__init__.py b/content/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/front/app/install/install.tsx b/front/app/install/install.tsx new file mode 100644 index 00000000..e5f060ea --- /dev/null +++ b/front/app/install/install.tsx @@ -0,0 +1,80 @@ +'use client' +import React, { use, useEffect } from 'react' +import { INSTALL_STEPS } from './steps/steps' +import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper' +import { useRouter, useSearchParams } from 'next/navigation' + + + + +function InstallClient() { + const searchParams = useSearchParams() + const router = useRouter() + const step: any = parseInt(searchParams.get('step') || '0'); + const [stepNumber, setStepNumber] = React.useState(step) + const [stepsState, setStepsState] = React.useState(INSTALL_STEPS) + + function handleStepChange(stepNumber: number) { + setStepNumber(stepNumber) + router.push(`/install?step=${stepNumber}`) + } + + useEffect(() => { + setStepNumber(step) + }, [step]) + + return ( + +
+
+ +
+
+
+ {stepsState.map((step, index) => ( +
handleStepChange(index)} + > +
+ {index} +
+
{step.name}
+ +
+ ))} +
+
+
+ +
+

{stepsState[stepNumber].name}

+
+ {stepsState[stepNumber].component} +
+
+
+ ) +} + +const LearnHouseLogo = () => { + return ( + + + + + + + + + + + + + + ) + +} + +export default InstallClient \ No newline at end of file diff --git a/front/app/install/page.tsx b/front/app/install/page.tsx new file mode 100644 index 00000000..18b90b4d --- /dev/null +++ b/front/app/install/page.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import InstallClient from './install' + + +export const metadata = { + title: "Install LearnHouse", + description: "Install Learnhouse on your server", +} + +function InstallPage() { + return ( +
+ +
+ ) +} + +export default InstallPage \ No newline at end of file diff --git a/front/app/install/steps/account_creation.tsx b/front/app/install/steps/account_creation.tsx new file mode 100644 index 00000000..7580d861 --- /dev/null +++ b/front/app/install/steps/account_creation.tsx @@ -0,0 +1,132 @@ +import FormLayout, { ButtonBlack, FormField, FormLabel, FormLabelAndMessage, FormMessage, Input } from '@components/StyledElements/Form/Form' +import * as Form from '@radix-ui/react-form'; +import { getAPIUrl } from '@services/config/config'; +import { createNewUserInstall, updateInstall } from '@services/install/install'; +import { swrFetcher } from '@services/utils/ts/requests'; +import { useFormik } from 'formik'; +import { useRouter } from 'next/navigation'; +import React from 'react' +import { BarLoader } from 'react-spinners'; +import useSWR, { mutate } from "swr"; + +const validate = (values: any) => { + const errors: any = {}; + + if (!values.email) { + errors.email = 'Required'; + } + else if ( + !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email) + ) { + errors.email = 'Invalid email address'; + } + + if (!values.password) { + errors.password = 'Required'; + } + else if (values.password.length < 8) { + errors.password = 'Password must be at least 8 characters'; + } + + if (!values.confirmPassword) { + errors.confirmPassword = 'Required'; + } + else if (values.confirmPassword !== values.password) { + errors.confirmPassword = 'Passwords must match'; + } + + if (!values.username) { + errors.username = 'Required'; + } + else if (values.username.length < 3) { + errors.username = 'Username must be at least 3 characters'; + } + + return errors; +}; + +function AccountCreation() { + const [isSubmitting, setIsSubmitting] = React.useState(false); + const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher); + const router = useRouter( + + ) + const formik = useFormik({ + initialValues: { + org_slug: '', + email: '', + password: '', + confirmPassword: '', + username: '', + }, + validate, + onSubmit: values => { + console.log(install.data[1].slug) + let finalvalues = { ...values, org_slug: install.data[1].slug } + let finalvalueswithoutpasswords = { ...values, password: '', confirmPassword: '', org_slug: install.data[1].slug } + let install_data = { ...install.data, 3: finalvalues } + let install_data_without_passwords = { ...install.data, 3: finalvalueswithoutpasswords } + updateInstall({ ...install_data_without_passwords }, 4) + createNewUserInstall(finalvalues) + + // await 2 seconds + setTimeout(() => { + setIsSubmitting(false) + }, 2000) + + router.push('/install?step=4') + + }, + }); + + return ( +
+ + + + + + + + {/* for password */} + + + + + + + + {/* for confirm password */} + + + + + + + + + {/* for username */} + + + + + + + + + +
+ + + {isSubmitting ? + : "Create Admin Account"} + + +
+ +
+
+ ) +} + +export default AccountCreation \ No newline at end of file diff --git a/front/app/install/steps/default_elements.tsx b/front/app/install/steps/default_elements.tsx new file mode 100644 index 00000000..04fe8a65 --- /dev/null +++ b/front/app/install/steps/default_elements.tsx @@ -0,0 +1,45 @@ +import { getAPIUrl } from '@services/config/config'; +import { createDefaultElements, updateInstall } from '@services/install/install'; +import { swrFetcher } from '@services/utils/ts/requests'; +import { useRouter } from 'next/navigation'; +import React from 'react' +import useSWR from "swr"; + +function DefaultElements() { + const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher); + const [isSubmitting, setIsSubmitting] = React.useState(false); + const [isSubmitted, setIsSubmitted] = React.useState(false); + const router = useRouter() + + function createDefElementsAndUpdateInstall() { + try { + createDefaultElements() + // add an {} to the install.data object + + let install_data = { ...install.data, 2: { status: 'OK' } } + + updateInstall(install_data, 3) + // await 2 seconds + setTimeout(() => { + setIsSubmitting(false) + }, 2000) + + router.push('/install?step=3') + setIsSubmitted(true) + } + catch (e) { + console.log(e) + } + } + + return ( +
+

Install Default Elements

+
+ Install +
+
+ ) +} + +export default DefaultElements \ No newline at end of file diff --git a/front/app/install/steps/disable_install_mode.tsx b/front/app/install/steps/disable_install_mode.tsx new file mode 100644 index 00000000..84acb6f2 --- /dev/null +++ b/front/app/install/steps/disable_install_mode.tsx @@ -0,0 +1,19 @@ +import { Check, Link } from 'lucide-react' +import React from 'react' + +function DisableInstallMode() { + return ( +
+
+ +
+

You have reached the end of the Installation process, please don't forget to disable installation mode.

+
+ + LearnHouse Docs +
+
+ ) +} + +export default DisableInstallMode \ No newline at end of file diff --git a/front/app/install/steps/finish.tsx b/front/app/install/steps/finish.tsx new file mode 100644 index 00000000..fbedc19e --- /dev/null +++ b/front/app/install/steps/finish.tsx @@ -0,0 +1,39 @@ +import { getAPIUrl } from '@services/config/config'; +import { updateInstall } from '@services/install/install'; +import { swrFetcher } from '@services/utils/ts/requests'; +import { Check } from 'lucide-react' +import { useRouter } from 'next/navigation'; +import React from 'react' +import useSWR from "swr"; + +const Finish = () => { + const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher); + const router = useRouter() + + async function finishInstall() { + console.log('install_data') + let install_data = { ...install.data, 5: { status: 'OK' } } + + let data = await updateInstall(install_data, 6) + if (data) { + router.push('/install?step=6') + } + else { + console.log('Error') + } + } + + return ( +
+

Installation Complete

+
+ +
+ Next Step +
+
+ + ) +} + +export default Finish \ No newline at end of file diff --git a/front/app/install/steps/get_started.tsx b/front/app/install/steps/get_started.tsx new file mode 100644 index 00000000..980912da --- /dev/null +++ b/front/app/install/steps/get_started.tsx @@ -0,0 +1,67 @@ +import PageLoading from '@components/Objects/Loaders/PageLoading'; +import { getAPIUrl } from '@services/config/config'; +import { swrFetcher } from '@services/utils/ts/requests'; +import { useRouter } from 'next/navigation'; +import React, { use, useEffect } from 'react' +import useSWR, { mutate } from "swr"; + +function GetStarted() { + const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher); + const router = useRouter() + + function startInstallation() { + fetch(`${getAPIUrl()}install/start`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({}) + }).then(res => res.json()).then(res => { + if (res.success) { + mutate(`${getAPIUrl()}install/latest`) + router.push(`/install?step=1`) + } + }) + + } + + function redirectToStep() { + const step = install.step + router.push(`/install?step=${step}`) + } + + + + useEffect(() => { + if (install) { + redirectToStep() + } + }, [install]) + + + if (error) return
+

Start a new installation

+
+ Start +
+
+ + if (isLoading) return + if (install) { + return ( +
+
+

You already started an installation

+
+ Continue +
+
+ Start +
+
+
+ ) + } +} + +export default GetStarted \ No newline at end of file diff --git a/front/app/install/steps/org_creation.tsx b/front/app/install/steps/org_creation.tsx new file mode 100644 index 00000000..58edc2d2 --- /dev/null +++ b/front/app/install/steps/org_creation.tsx @@ -0,0 +1,138 @@ + +import FormLayout, { ButtonBlack, FormField, FormLabel, FormLabelAndMessage, FormMessage, Input } from '@components/StyledElements/Form/Form' +import * as Form from '@radix-ui/react-form'; +import { useFormik } from 'formik'; +import { BarLoader } from 'react-spinners'; +import React from 'react' +import { createNewOrganization } from '@services/organizations/orgs'; +import { swrFetcher } from '@services/utils/ts/requests'; +import { getAPIUrl } from '@services/config/config'; +import useSWR, { mutate } from "swr"; +import { createNewOrgInstall, updateInstall } from '@services/install/install'; +import { useRouter } from 'next/navigation'; +import { Check } from 'lucide-react'; + +const validate = (values: any) => { + const errors: any = {}; + + if (!values.name) { + errors.name = 'Required'; + } + + if (!values.description) { + errors.description = 'Required'; + } + + if (!values.slug) { + errors.slug = 'Required'; + } + + if (!values.email) { + errors.email = 'Required'; + } + else if ( + !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email) + ) { + errors.email = 'Invalid email address'; + } + + + + return errors; +}; + +function OrgCreation() { + const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher); + const [isSubmitting, setIsSubmitting] = React.useState(false); + const [isSubmitted, setIsSubmitted] = React.useState(false); + const router = useRouter() + + + function createOrgAndUpdateInstall(values: any) { + try { + createNewOrgInstall(values) + install.data = { + 1: values + } + let install_data = { ...install.data, 1: values } + updateInstall(install_data, 2) + // await 2 seconds + setTimeout(() => { + setIsSubmitting(false) + }, 2000) + + router.push('/install?step=2') + setIsSubmitted(true) + } + catch (e) { + console.log(e) + } + + } + + const formik = useFormik({ + initialValues: { + name: '', + description: '', + slug: '', + email: '', + }, + validate, + onSubmit: values => { + createOrgAndUpdateInstall(values) + }, + }); + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + {/* for username */} + + + + + + + + + +
+ + + {isSubmitting ? + : "Create Organization"} + + +
+ + {isSubmitted &&
Organization Created Successfully
} + + +
+
+ ) +} + +export default OrgCreation \ No newline at end of file diff --git a/front/app/install/steps/sample_data.tsx b/front/app/install/steps/sample_data.tsx new file mode 100644 index 00000000..d89db078 --- /dev/null +++ b/front/app/install/steps/sample_data.tsx @@ -0,0 +1,43 @@ +import { getAPIUrl } from '@services/config/config'; +import { createSampleDataInstall, updateInstall } from '@services/install/install'; +import { swrFetcher } from '@services/utils/ts/requests'; +import { useRouter } from 'next/navigation'; +import React from 'react' +import useSWR, { mutate } from "swr"; + +function SampleData() { + const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher); + const router = useRouter() + + function createSampleData() { + + try { + let username = install.data[3].username + let slug = install.data[1].slug + console.log(install.data) + createSampleDataInstall(username, slug) + + let install_data = { ...install.data, 4: { status: 'OK' } } + updateInstall(install_data, 5) + + router.push('/install?step=5') + + } + catch (e) { + console.log(e) + } + } + + + + return ( +
+

Install Sample data on your organization

+
+ Start +
+
+ ) +} + +export default SampleData \ No newline at end of file diff --git a/front/app/install/steps/steps.tsx b/front/app/install/steps/steps.tsx new file mode 100644 index 00000000..2a55de13 --- /dev/null +++ b/front/app/install/steps/steps.tsx @@ -0,0 +1,53 @@ +import AccountCreation from "./account_creation"; +import DefaultElements from "./default_elements"; +import DisableInstallMode from "./disable_install_mode"; +import Finish from "./finish"; +import GetStarted from "./get_started"; +import OrgCreation from "./org_creation"; +import SampleData from "./sample_data"; + +export const INSTALL_STEPS = [ + { + id: "INSTALL_STATUS", + name: "Get started", + component: , + completed: false, + }, + { + id: "ORGANIZATION_CREATION", + name: "Organization Creation", + component: , + completed: false, + }, + { + id: "DEFAULT_ELEMENTS", + name: "Default Elements", + component: , + completed: false, + }, + { + id: "ACCOUNT_CREATION", + name: "Account Creation", + component: , + completed: false, + }, + { + id: "SAMPLE_DATA", + name: "Sample Data", + component: , + completed: false, + }, + { + id: "FINISH", + name: "Finish", + component: , + completed: false, + + }, + { + id: "DISABLING_INSTALLATION_MODE", + name: "Disabling Installation Mode", + component: , + completed: false, + }, +]; diff --git a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/activity.tsx b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/activity.tsx index 77d3319f..4086f673 100644 --- a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/activity.tsx +++ b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/activity.tsx @@ -107,7 +107,7 @@ export function MarkStatus(props: { activityid: string, course: any, orgslug: st router.refresh(); // refresh page (FIX for Next.js BUG) - window.location.reload(); + //window.location.reload(); } diff --git a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx index d1aafc72..41c999b1 100644 --- a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx +++ b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx @@ -27,7 +27,7 @@ const CourseClient = (props: any) => { router.refresh(); // refresh page (FIX for Next.js BUG) - window.location.reload(); + // window.location.reload(); } async function quitCourse() { @@ -38,7 +38,7 @@ const CourseClient = (props: any) => { router.refresh(); // refresh page (FIX for Next.js BUG) - window.location.reload(); + //window.location.reload(); } @@ -62,7 +62,7 @@ const CourseClient = (props: any) => { -
+

Description

diff --git a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/page.tsx b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/page.tsx index 5bbecf64..5bf52f01 100644 --- a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/page.tsx +++ b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/page.tsx @@ -16,6 +16,7 @@ import Modal from "@components/StyledElements/Modal/Modal"; import { denyAccessToUser } from "@services/utils/react/middlewares/views"; import { Folders, Package2, SaveIcon } from "lucide-react"; import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper"; +import { revalidateTags } from "@services/utils/ts/requests"; function CourseEdit(params: any) { @@ -75,6 +76,8 @@ function CourseEdit(params: any) { const submitChapter = async (chapter: any) => { await createChapter(chapter, courseid); await getCourseChapters(); + revalidateTags(['courses'], orgslug); + router.refresh(); setNewChapterModal(false); }; @@ -86,6 +89,8 @@ function CourseEdit(params: any) { await createActivity(activity, activity.chapterId, org.org_id); await getCourseChapters(); setNewActivityModal(false); + revalidateTags(['courses'], orgslug); + router.refresh(); }; // Submit File Upload @@ -94,6 +99,8 @@ function CourseEdit(params: any) { await createFileActivity(file, type, activity, chapterId); await getCourseChapters(); setNewActivityModal(false); + revalidateTags(['courses'], orgslug); + router.refresh(); }; // Submit YouTube Video Upload @@ -103,17 +110,23 @@ function CourseEdit(params: any) { await createExternalVideoActivity(external_video_data, activity, chapterId); await getCourseChapters(); setNewActivityModal(false); + revalidateTags(['courses'], orgslug); + router.refresh(); }; const deleteChapterUI = async (chapterId: any) => { console.log("deleteChapter", chapterId); await deleteChapter(chapterId); getCourseChapters(); + revalidateTags(['courses'], orgslug); + router.refresh(); }; const updateChapters = () => { console.log(data); updateChaptersMetadata(courseid, data); + revalidateTags(['courses'], orgslug); + router.refresh(); }; /* diff --git a/front/app/orgs/[orgslug]/(withmenu)/trail/trail.tsx b/front/app/orgs/[orgslug]/(withmenu)/trail/trail.tsx index 38015cce..d19a8c78 100644 --- a/front/app/orgs/[orgslug]/(withmenu)/trail/trail.tsx +++ b/front/app/orgs/[orgslug]/(withmenu)/trail/trail.tsx @@ -22,7 +22,12 @@ function Trail(params: any) { ) : (
{trail.courses.map((course: any) => ( - + !course.masked ? ( + + ) : ( + <> + ) + ))}
diff --git a/front/components/Objects/Editor/Editor.tsx b/front/components/Objects/Editor/Editor.tsx index 9bcd84d2..0b44e7a6 100644 --- a/front/components/Objects/Editor/Editor.tsx +++ b/front/components/Objects/Editor/Editor.tsx @@ -25,6 +25,7 @@ import PDFBlock from "./Extensions/PDF/PDFBlock"; import QuizBlock from "./Extensions/Quiz/QuizBlock"; import ToolTip from "@components/StyledElements/Tooltip/Tooltip"; import Link from "next/link"; +import { getCourseThumbnailMediaDirectory } from "@services/media/media"; interface Editor { content: string; @@ -120,7 +121,7 @@ function Editor(props: Editor) { - + {" "} diff --git a/front/components/Objects/Modals/Course/Create/CreateCourse.tsx b/front/components/Objects/Modals/Course/Create/CreateCourse.tsx index e9870b9f..dd92a7d1 100644 --- a/front/components/Objects/Modals/Course/Create/CreateCourse.tsx +++ b/front/components/Objects/Modals/Course/Create/CreateCourse.tsx @@ -49,9 +49,10 @@ function CreateCourseModal({ closeModal, orgslug }: any) { if (status.org_id == orgId) { closeModal(); router.refresh(); + revalidateTags(['courses'], orgslug); // refresh page (FIX for Next.js BUG) - window.location.reload(); + // window.location.reload(); } else { alert("Error creating course, please see console logs"); console.log(status); diff --git a/front/components/Pages/Trail/TrailCourseElement.tsx b/front/components/Pages/Trail/TrailCourseElement.tsx index 57cef41d..67332711 100644 --- a/front/components/Pages/Trail/TrailCourseElement.tsx +++ b/front/components/Pages/Trail/TrailCourseElement.tsx @@ -4,6 +4,7 @@ import { removeCourse } from '@services/courses/activity'; import { getCourseThumbnailMediaDirectory } from '@services/media/media'; import { revalidateTags } from '@services/utils/ts/requests'; import Link from 'next/link'; +import { useRouter } from 'next/navigation'; import { mutate } from 'swr'; interface TrailCourseElementProps { @@ -14,12 +15,14 @@ interface TrailCourseElementProps { function TrailCourseElement(props: TrailCourseElementProps) { const courseid = props.course.course_id.replace("course_", "") const course = props.course + const router = useRouter(); async function quitCourse(course_id: string) { // Close activity let activity = await removeCourse(course_id, props.orgslug); // Mutate course revalidateTags(['courses'], props.orgslug); + router.refresh(); // Mutate mutate(`${getAPIUrl()}trail/org_slug/${props.orgslug}/trail`); diff --git a/front/components/Security/AuthenticatedClientElement.tsx b/front/components/Security/AuthenticatedClientElement.tsx index 6ced1f88..e46618bd 100644 --- a/front/components/Security/AuthenticatedClientElement.tsx +++ b/front/components/Security/AuthenticatedClientElement.tsx @@ -14,9 +14,9 @@ export const AuthenticatedClientElement = (props: AuthenticatedClientElementProp // Available roles const org_roles_values = ["admin", "owner"]; - const user_roles_values = ["role_admin"]; + const user_roles_values = ["role_admin", "role_super_admin"]; + - function checkRoles() { const org_id = props.orgId; diff --git a/front/components/StyledElements/Form/Form.tsx b/front/components/StyledElements/Form/Form.tsx index 17f84702..f038aefc 100644 --- a/front/components/StyledElements/Form/Form.tsx +++ b/front/components/StyledElements/Form/Form.tsx @@ -2,6 +2,7 @@ import React from 'react'; import * as Form from '@radix-ui/react-form'; import { styled, keyframes } from '@stitches/react'; import { blackA, violet, mauve } from '@radix-ui/colors'; +import { Info } from 'lucide-react'; const FormLayout = (props: any, onSubmit: any) => ( @@ -9,6 +10,13 @@ const FormLayout = (props: any, onSubmit: any) => ( ); +export const FormLabelAndMessage = (props: { label: string, message?: string }) => ( +
+ {props.label} + {props.message &&
{props.message}
|| <>} +
+); + export const FormRoot = styled(Form.Root, { margin: 7 }); diff --git a/front/middleware.ts b/front/middleware.ts index 4a8f5280..ac547697 100644 --- a/front/middleware.ts +++ b/front/middleware.ts @@ -1,3 +1,4 @@ +import { isInstallModeEnabled } from "@services/install/install"; import { LEARNHOUSE_DOMAIN, getDefaultOrg, isMultiOrgModeEnabled } from "./services/config/config"; import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; @@ -16,7 +17,7 @@ export const config = { ], }; -export default function middleware(req: NextRequest) { +export default async function middleware(req: NextRequest) { // Get initial data const hosting_mode = isMultiOrgModeEnabled() ? "multi" : "single"; const default_org = getDefaultOrg(); @@ -28,6 +29,17 @@ export default function middleware(req: NextRequest) { return NextResponse.rewrite(new URL(pathname, req.url)); } + // Install Page + if (pathname.startsWith("/install")) { + // Check if install mode is enabled + const install_mode = await isInstallModeEnabled(); + if (install_mode) { + return NextResponse.rewrite(new URL(pathname, req.url)); + } else { + return NextResponse.redirect(new URL("/", req.url)); + } + } + // Dynamic Pages Editor if (pathname.match(/^\/course\/[^/]+\/activity\/[^/]+\/edit$/)) { return NextResponse.rewrite(new URL(`/editor${pathname}`, req.url)); diff --git a/front/package-lock.json b/front/package-lock.json index fa7645e5..5ca159b2 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -26,7 +26,7 @@ "formik": "^2.2.9", "framer-motion": "^7.3.6", "lucide-react": "^0.248.0", - "next": "^13.4.7-canary.4", + "next": "^13.4.8", "re-resizable": "^6.9.9", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", @@ -2146,9 +2146,9 @@ } }, "node_modules/@next/env": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.7.tgz", - "integrity": "sha512-ZlbiFulnwiFsW9UV1ku1OvX/oyIPLtMk9p/nnvDSwI0s7vSoZdRtxXNsaO+ZXrLv/pMbXVGq4lL8TbY9iuGmVw==" + "version": "13.4.8", + "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.8.tgz", + "integrity": "sha512-twuSf1klb3k9wXI7IZhbZGtFCWvGD4wXTY2rmvzIgVhXhs7ISThrbNyutBx3jWIL8Y/Hk9+woytFz5QsgtcRKQ==" }, "node_modules/@next/eslint-plugin-next": { "version": "13.0.6", @@ -2160,9 +2160,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.7.tgz", - "integrity": "sha512-VZTxPv1b59KGiv/pZHTO5Gbsdeoxcj2rU2cqJu03btMhHpn3vwzEK0gUSVC/XW96aeGO67X+cMahhwHzef24/w==", + "version": "13.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.8.tgz", + "integrity": "sha512-MSFplVM4dTWOuKAUv0XR9gY7AWtMSBu9os9f+kp+s5rWhM1I2CdR3obFttd6366nS/W/VZxbPM5oEIdlIa46zA==", "cpu": [ "arm64" ], @@ -2175,9 +2175,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.7.tgz", - "integrity": "sha512-gO2bw+2Ymmga+QYujjvDz9955xvYGrWofmxTq7m70b9pDPvl7aDFABJOZ2a8SRCuSNB5mXU8eTOmVVwyp/nAew==", + "version": "13.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.8.tgz", + "integrity": "sha512-Reox+UXgonon9P0WNDE6w85DGtyBqGitl/ryznOvn6TvfxEaZIpTgeu3ZrJLU9dHSMhiK7YAM793mE/Zii2/Qw==", "cpu": [ "x64" ], @@ -2190,9 +2190,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.7.tgz", - "integrity": "sha512-6cqp3vf1eHxjIDhEOc7Mh/s8z1cwc/l5B6ZNkOofmZVyu1zsbEM5Hmx64s12Rd9AYgGoiCz4OJ4M/oRnkE16/Q==", + "version": "13.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.8.tgz", + "integrity": "sha512-kdyzYvAYtqQVgzIKNN7e1rLU8aZv86FDSRqPlOkKZlvqudvTO0iohuTPmnEEDlECeBM6qRPShNffotDcU/R2KA==", "cpu": [ "arm64" ], @@ -2205,9 +2205,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.7.tgz", - "integrity": "sha512-T1kD2FWOEy5WPidOn1si0rYmWORNch4a/NR52Ghyp4q7KyxOCuiOfZzyhVC5tsLIBDH3+cNdB5DkD9afpNDaOw==", + "version": "13.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.8.tgz", + "integrity": "sha512-oWxx4yRkUGcR81XwbI+T0zhZ3bDF6V1aVLpG+C7hSG50ULpV8gC39UxVO22/bv93ZlcfMY4zl8xkz9Klct6dpQ==", "cpu": [ "arm64" ], @@ -2220,9 +2220,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.7.tgz", - "integrity": "sha512-zaEC+iEiAHNdhl6fuwl0H0shnTzQoAoJiDYBUze8QTntE/GNPfTYpYboxF5LRYIjBwETUatvE0T64W6SKDipvg==", + "version": "13.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.8.tgz", + "integrity": "sha512-anhtvuO6eE9YRhYnaEGTfbpH3L5gT/9qPFcNoi6xS432r/4DAtpJY8kNktqkTVevVIC/pVumqO8tV59PR3zbNg==", "cpu": [ "x64" ], @@ -2235,9 +2235,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.7.tgz", - "integrity": "sha512-X6r12F8d8SKAtYJqLZBBMIwEqcTRvUdVm+xIq+l6pJqlgT2tNsLLf2i5Cl88xSsIytBICGsCNNHd+siD2fbWBA==", + "version": "13.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.8.tgz", + "integrity": "sha512-aR+J4wWfNgH1DwCCBNjan7Iumx0lLtn+2/rEYuhIrYLY4vnxqSVGz9u3fXcgUwo6Q9LT8NFkaqK1vPprdq+BXg==", "cpu": [ "x64" ], @@ -2250,9 +2250,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.7.tgz", - "integrity": "sha512-NPnmnV+vEIxnu6SUvjnuaWRglZzw4ox5n/MQTxeUhb5iwVWFedolPFebMNwgrWu4AELwvTdGtWjqof53AiWHcw==", + "version": "13.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.8.tgz", + "integrity": "sha512-OWBKIrJwQBTqrat0xhxEB/jcsjJR3+diD9nc/Y8F1mRdQzsn4bPsomgJyuqPVZs6Lz3K18qdIkvywmfSq75SsQ==", "cpu": [ "arm64" ], @@ -2265,9 +2265,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.7.tgz", - "integrity": "sha512-6Hxijm6/a8XqLQpOOf/XuwWRhcuc/g4rBB2oxjgCMuV9Xlr2bLs5+lXyh8w9YbAUMYR3iC9mgOlXbHa79elmXw==", + "version": "13.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.8.tgz", + "integrity": "sha512-agiPWGjUndXGTOn4ChbKipQXRA6/UPkywAWIkx7BhgGv48TiJfHTK6MGfBoL9tS6B4mtW39++uy0wFPnfD0JWg==", "cpu": [ "ia32" ], @@ -2280,9 +2280,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.7.tgz", - "integrity": "sha512-sW9Yt36Db1nXJL+mTr2Wo0y+VkPWeYhygvcHj1FF0srVtV+VoDjxleKtny21QHaG05zdeZnw2fCtf2+dEqgwqA==", + "version": "13.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.8.tgz", + "integrity": "sha512-UIRKoByVKbuR6SnFG4JM8EMFlJrfEGuUQ1ihxzEleWcNwRMMiVaCj1KyqfTOW8VTQhJ0u8P1Ngg6q1RwnIBTtw==", "cpu": [ "x64" ], @@ -6379,11 +6379,11 @@ "dev": true }, "node_modules/next": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/next/-/next-13.4.7.tgz", - "integrity": "sha512-M8z3k9VmG51SRT6v5uDKdJXcAqLzP3C+vaKfLIAM0Mhx1um1G7MDnO63+m52qPdZfrTFzMZNzfsgvm3ghuVHIQ==", + "version": "13.4.8", + "resolved": "https://registry.npmjs.org/next/-/next-13.4.8.tgz", + "integrity": "sha512-lxUjndYKjZHGK3CWeN2RI+/6ni6EUvjiqGWXAYPxUfGIdFGQ5XoisrqAJ/dF74aP27buAfs8MKIbIMMdxjqSBg==", "dependencies": { - "@next/env": "13.4.7", + "@next/env": "13.4.8", "@swc/helpers": "0.5.1", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001406", @@ -6399,15 +6399,15 @@ "node": ">=16.8.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "13.4.7", - "@next/swc-darwin-x64": "13.4.7", - "@next/swc-linux-arm64-gnu": "13.4.7", - "@next/swc-linux-arm64-musl": "13.4.7", - "@next/swc-linux-x64-gnu": "13.4.7", - "@next/swc-linux-x64-musl": "13.4.7", - "@next/swc-win32-arm64-msvc": "13.4.7", - "@next/swc-win32-ia32-msvc": "13.4.7", - "@next/swc-win32-x64-msvc": "13.4.7" + "@next/swc-darwin-arm64": "13.4.8", + "@next/swc-darwin-x64": "13.4.8", + "@next/swc-linux-arm64-gnu": "13.4.8", + "@next/swc-linux-arm64-musl": "13.4.8", + "@next/swc-linux-x64-gnu": "13.4.8", + "@next/swc-linux-x64-musl": "13.4.8", + "@next/swc-win32-arm64-msvc": "13.4.8", + "@next/swc-win32-ia32-msvc": "13.4.8", + "@next/swc-win32-x64-msvc": "13.4.8" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -10063,9 +10063,9 @@ } }, "@next/env": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.7.tgz", - "integrity": "sha512-ZlbiFulnwiFsW9UV1ku1OvX/oyIPLtMk9p/nnvDSwI0s7vSoZdRtxXNsaO+ZXrLv/pMbXVGq4lL8TbY9iuGmVw==" + "version": "13.4.8", + "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.8.tgz", + "integrity": "sha512-twuSf1klb3k9wXI7IZhbZGtFCWvGD4wXTY2rmvzIgVhXhs7ISThrbNyutBx3jWIL8Y/Hk9+woytFz5QsgtcRKQ==" }, "@next/eslint-plugin-next": { "version": "13.0.6", @@ -10077,57 +10077,57 @@ } }, "@next/swc-darwin-arm64": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.7.tgz", - "integrity": "sha512-VZTxPv1b59KGiv/pZHTO5Gbsdeoxcj2rU2cqJu03btMhHpn3vwzEK0gUSVC/XW96aeGO67X+cMahhwHzef24/w==", + "version": "13.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.8.tgz", + "integrity": "sha512-MSFplVM4dTWOuKAUv0XR9gY7AWtMSBu9os9f+kp+s5rWhM1I2CdR3obFttd6366nS/W/VZxbPM5oEIdlIa46zA==", "optional": true }, "@next/swc-darwin-x64": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.7.tgz", - "integrity": "sha512-gO2bw+2Ymmga+QYujjvDz9955xvYGrWofmxTq7m70b9pDPvl7aDFABJOZ2a8SRCuSNB5mXU8eTOmVVwyp/nAew==", + "version": "13.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.8.tgz", + "integrity": "sha512-Reox+UXgonon9P0WNDE6w85DGtyBqGitl/ryznOvn6TvfxEaZIpTgeu3ZrJLU9dHSMhiK7YAM793mE/Zii2/Qw==", "optional": true }, "@next/swc-linux-arm64-gnu": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.7.tgz", - "integrity": "sha512-6cqp3vf1eHxjIDhEOc7Mh/s8z1cwc/l5B6ZNkOofmZVyu1zsbEM5Hmx64s12Rd9AYgGoiCz4OJ4M/oRnkE16/Q==", + "version": "13.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.8.tgz", + "integrity": "sha512-kdyzYvAYtqQVgzIKNN7e1rLU8aZv86FDSRqPlOkKZlvqudvTO0iohuTPmnEEDlECeBM6qRPShNffotDcU/R2KA==", "optional": true }, "@next/swc-linux-arm64-musl": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.7.tgz", - "integrity": "sha512-T1kD2FWOEy5WPidOn1si0rYmWORNch4a/NR52Ghyp4q7KyxOCuiOfZzyhVC5tsLIBDH3+cNdB5DkD9afpNDaOw==", + "version": "13.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.8.tgz", + "integrity": "sha512-oWxx4yRkUGcR81XwbI+T0zhZ3bDF6V1aVLpG+C7hSG50ULpV8gC39UxVO22/bv93ZlcfMY4zl8xkz9Klct6dpQ==", "optional": true }, "@next/swc-linux-x64-gnu": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.7.tgz", - "integrity": "sha512-zaEC+iEiAHNdhl6fuwl0H0shnTzQoAoJiDYBUze8QTntE/GNPfTYpYboxF5LRYIjBwETUatvE0T64W6SKDipvg==", + "version": "13.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.8.tgz", + "integrity": "sha512-anhtvuO6eE9YRhYnaEGTfbpH3L5gT/9qPFcNoi6xS432r/4DAtpJY8kNktqkTVevVIC/pVumqO8tV59PR3zbNg==", "optional": true }, "@next/swc-linux-x64-musl": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.7.tgz", - "integrity": "sha512-X6r12F8d8SKAtYJqLZBBMIwEqcTRvUdVm+xIq+l6pJqlgT2tNsLLf2i5Cl88xSsIytBICGsCNNHd+siD2fbWBA==", + "version": "13.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.8.tgz", + "integrity": "sha512-aR+J4wWfNgH1DwCCBNjan7Iumx0lLtn+2/rEYuhIrYLY4vnxqSVGz9u3fXcgUwo6Q9LT8NFkaqK1vPprdq+BXg==", "optional": true }, "@next/swc-win32-arm64-msvc": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.7.tgz", - "integrity": "sha512-NPnmnV+vEIxnu6SUvjnuaWRglZzw4ox5n/MQTxeUhb5iwVWFedolPFebMNwgrWu4AELwvTdGtWjqof53AiWHcw==", + "version": "13.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.8.tgz", + "integrity": "sha512-OWBKIrJwQBTqrat0xhxEB/jcsjJR3+diD9nc/Y8F1mRdQzsn4bPsomgJyuqPVZs6Lz3K18qdIkvywmfSq75SsQ==", "optional": true }, "@next/swc-win32-ia32-msvc": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.7.tgz", - "integrity": "sha512-6Hxijm6/a8XqLQpOOf/XuwWRhcuc/g4rBB2oxjgCMuV9Xlr2bLs5+lXyh8w9YbAUMYR3iC9mgOlXbHa79elmXw==", + "version": "13.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.8.tgz", + "integrity": "sha512-agiPWGjUndXGTOn4ChbKipQXRA6/UPkywAWIkx7BhgGv48TiJfHTK6MGfBoL9tS6B4mtW39++uy0wFPnfD0JWg==", "optional": true }, "@next/swc-win32-x64-msvc": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.7.tgz", - "integrity": "sha512-sW9Yt36Db1nXJL+mTr2Wo0y+VkPWeYhygvcHj1FF0srVtV+VoDjxleKtny21QHaG05zdeZnw2fCtf2+dEqgwqA==", + "version": "13.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.8.tgz", + "integrity": "sha512-UIRKoByVKbuR6SnFG4JM8EMFlJrfEGuUQ1ihxzEleWcNwRMMiVaCj1KyqfTOW8VTQhJ0u8P1Ngg6q1RwnIBTtw==", "optional": true }, "@nicolo-ribaudo/chokidar-2": { @@ -13083,20 +13083,20 @@ "dev": true }, "next": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/next/-/next-13.4.7.tgz", - "integrity": "sha512-M8z3k9VmG51SRT6v5uDKdJXcAqLzP3C+vaKfLIAM0Mhx1um1G7MDnO63+m52qPdZfrTFzMZNzfsgvm3ghuVHIQ==", + "version": "13.4.8", + "resolved": "https://registry.npmjs.org/next/-/next-13.4.8.tgz", + "integrity": "sha512-lxUjndYKjZHGK3CWeN2RI+/6ni6EUvjiqGWXAYPxUfGIdFGQ5XoisrqAJ/dF74aP27buAfs8MKIbIMMdxjqSBg==", "requires": { - "@next/env": "13.4.7", - "@next/swc-darwin-arm64": "13.4.7", - "@next/swc-darwin-x64": "13.4.7", - "@next/swc-linux-arm64-gnu": "13.4.7", - "@next/swc-linux-arm64-musl": "13.4.7", - "@next/swc-linux-x64-gnu": "13.4.7", - "@next/swc-linux-x64-musl": "13.4.7", - "@next/swc-win32-arm64-msvc": "13.4.7", - "@next/swc-win32-ia32-msvc": "13.4.7", - "@next/swc-win32-x64-msvc": "13.4.7", + "@next/env": "13.4.8", + "@next/swc-darwin-arm64": "13.4.8", + "@next/swc-darwin-x64": "13.4.8", + "@next/swc-linux-arm64-gnu": "13.4.8", + "@next/swc-linux-arm64-musl": "13.4.8", + "@next/swc-linux-x64-gnu": "13.4.8", + "@next/swc-linux-x64-musl": "13.4.8", + "@next/swc-win32-arm64-msvc": "13.4.8", + "@next/swc-win32-ia32-msvc": "13.4.8", + "@next/swc-win32-x64-msvc": "13.4.8", "@swc/helpers": "0.5.1", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001406", diff --git a/front/package.json b/front/package.json index c5b09c2d..cb2eec57 100644 --- a/front/package.json +++ b/front/package.json @@ -27,7 +27,7 @@ "formik": "^2.2.9", "framer-motion": "^7.3.6", "lucide-react": "^0.248.0", - "next": "^13.4.7-canary.4", + "next": "^13.4.8", "re-resizable": "^6.9.9", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", diff --git a/front/services/install/install.ts b/front/services/install/install.ts new file mode 100644 index 00000000..ab69dc22 --- /dev/null +++ b/front/services/install/install.ts @@ -0,0 +1,42 @@ +import { getAPIUrl } from "@services/config/config"; +import { RequestBody, errorHandling } from "@services/utils/ts/requests"; + +export async function updateInstall(body: any, step: number) { + const result = await fetch(`${getAPIUrl()}install/update?step=${step}`, RequestBody("POST", body, null)); + const res = await errorHandling(result); + return res; +} + +export async function createNewOrgInstall(body: any) { + const result = await fetch(`${getAPIUrl()}install/org`, RequestBody("POST", body, null)); + const res = await errorHandling(result); + return res; +} + +export async function createNewUserInstall(body: any) { + const result = await fetch(`${getAPIUrl()}install/user?org_slug=${body.org_slug}`, RequestBody("POST", body, null)); + const res = await errorHandling(result); + return res; +} + +export async function createSampleDataInstall(username: string, org_slug: string) { + const result = await fetch(`${getAPIUrl()}install/sample?username=${username}&org_slug=${org_slug}`, RequestBody("POST", null, null)); + const res = await errorHandling(result); + return res; +} + +export async function createDefaultElements() { + const result = await fetch(`${getAPIUrl()}install/default_elements`, RequestBody("POST", null, null)); + const res = await errorHandling(result); + return res; +} + +export async function isInstallModeEnabled() { + const result = await fetch(`${getAPIUrl()}install/latest`, RequestBody("GET", null, null)); + if (result.status === 200) { + return true; + } + else { + return false; + } +} diff --git a/src/core/events/content.py b/src/core/events/content.py new file mode 100644 index 00000000..f4ca4353 --- /dev/null +++ b/src/core/events/content.py @@ -0,0 +1,8 @@ +import os + + +async def check_content_directory(): + if not os.path.exists("content"): + # create folder for activity + print("Creating content directory...") + os.makedirs("content") diff --git a/src/core/events/events.py b/src/core/events/events.py index bc95d16e..f5a552e0 100644 --- a/src/core/events/events.py +++ b/src/core/events/events.py @@ -1,6 +1,7 @@ from typing import Callable from fastapi import FastAPI from config.config import LearnHouseConfig, get_learnhouse_config +from src.core.events.content import check_content_directory from src.core.events.database import close_database, connect_to_db from src.core.events.logs import create_logs_dir from src.core.events.sentry import init_sentry @@ -10,7 +11,7 @@ def startup_app(app: FastAPI) -> Callable: async def start_app() -> None: # Get LearnHouse Config learnhouse_config: LearnHouseConfig = get_learnhouse_config() - app.learnhouse_config = learnhouse_config # type: ignore + app.learnhouse_config = learnhouse_config # type: ignore # Init Sentry await init_sentry(app) @@ -21,10 +22,14 @@ def startup_app(app: FastAPI) -> Callable: # Create logs directory await create_logs_dir() + # Create content directory + await check_content_directory() + return start_app def shutdown_app(app: FastAPI) -> Callable: async def close_app() -> None: await close_database(app) + return close_app diff --git a/src/router.py b/src/router.py index 66f8b633..895873b3 100644 --- a/src/router.py +++ b/src/router.py @@ -1,7 +1,9 @@ from fastapi import APIRouter, Depends from src.routers import blocks, dev, trail, users, auth, orgs, roles from src.routers.courses import chapters, collections, courses, activities -from src.services.dev.dev import isDevModeEnabled +from src.routers.install import install +from src.services.dev.dev import isDevModeEnabledOrRaise +from src.services.install.install import isInstallModeEnabled v1_router = APIRouter(prefix="/api/v1") @@ -16,10 +18,20 @@ v1_router.include_router(blocks.router, prefix="/blocks", tags=["blocks"]) v1_router.include_router(courses.router, prefix="/courses", tags=["courses"]) v1_router.include_router(chapters.router, prefix="/chapters", tags=["chapters"]) v1_router.include_router(activities.router, prefix="/activities", tags=["activities"]) -v1_router.include_router( collections.router, prefix="/collections", tags=["collections"]) +v1_router.include_router( + collections.router, prefix="/collections", tags=["collections"] +) v1_router.include_router(trail.router, prefix="/trail", tags=["trail"]) # Dev Routes v1_router.include_router( - dev.router, prefix="/dev", tags=["dev"], dependencies=[Depends(isDevModeEnabled)] + dev.router, prefix="/dev", tags=["dev"], dependencies=[Depends(isDevModeEnabledOrRaise)] +) + +# Install Routes +v1_router.include_router( + install.router, + prefix="/install", + tags=["install"], + dependencies=[Depends(isInstallModeEnabled)], ) diff --git a/src/routers/install/__init__.py b/src/routers/install/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/routers/install/install.py b/src/routers/install/install.py new file mode 100644 index 00000000..52f6d5f2 --- /dev/null +++ b/src/routers/install/install.py @@ -0,0 +1,70 @@ +from fastapi import APIRouter, Request + +from src.services.install.install import ( + create_install_instance, + create_sample_data, + get_latest_install_instance, + install_create_organization, + install_create_organization_user, + install_default_elements, + update_install_instance, +) +from src.services.orgs.schemas.orgs import Organization +from src.services.users.schemas.users import UserWithPassword + + +router = APIRouter() + + +@router.post("/start") +async def api_create_install_instance(request: Request, data: dict): + # create install + install = await create_install_instance(request, data) + + return install + + +@router.get("/latest") +async def api_get_latest_install_instance(request: Request): + # get latest created install + install = await get_latest_install_instance(request) + + return install + + +@router.post("/default_elements") +async def api_install_def_elements(request: Request): + elements = await install_default_elements(request, {}) + + return elements + + +@router.post("/org") +async def api_install_org(request: Request, org: Organization): + organization = await install_create_organization(request, org) + + return organization + + +@router.post("/user") +async def api_install_user(request: Request, data: UserWithPassword, org_slug: str): + user = await install_create_organization_user(request, data, org_slug) + + return user + + +@router.post("/sample") +async def api_install_user_sample(request: Request, username: str, org_slug: str): + sample = await create_sample_data(org_slug, username, request) + + return sample + + +@router.post("/update") +async def api_update_install_instance(request: Request, data: dict, step: int): + request.app.db["installs"] + + # get latest created install + install = await update_install_instance(request, data, step) + + return install diff --git a/src/routers/trail.py b/src/routers/trail.py index aac85c77..f2eb38b5 100644 --- a/src/routers/trail.py +++ b/src/routers/trail.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, Depends, Request from src.security.auth import get_current_user -from src.services.trail import Trail, add_activity_to_trail, add_course_to_trail, create_trail, get_user_trail_with_orgslug, get_user_trail, remove_course_from_trail +from src.services.trail.trail import Trail, add_activity_to_trail, add_course_to_trail, create_trail, get_user_trail_with_orgslug, get_user_trail, remove_course_from_trail router = APIRouter() diff --git a/src/security/security.py b/src/security/security.py index b5918389..fdd58323 100644 --- a/src/security/security.py +++ b/src/security/security.py @@ -133,7 +133,7 @@ async def check_user_role_org_with_element_org( # Check if The role belongs to the same organization as the element role_db = await roles.find_one({"role_id": role.role_id}) role = RoleInDB(**role_db) - if role.org_id == element_org["org_id"] or role.org_id == "*": + if (role.org_id == element_org["org_id"]) or role.org_id == "*": # Check if user has the right role for role in roles_list: role_db = await roles.find_one({"role_id": role.role_id}) diff --git a/src/services/courses/activities/activities.py b/src/services/courses/activities/activities.py index 0a1c704c..57d1d410 100644 --- a/src/services/courses/activities/activities.py +++ b/src/services/courses/activities/activities.py @@ -148,6 +148,7 @@ async def update_activity( coursechapter_id=activity["coursechapter_id"], creationDate=creationDate, updateDate=str(datetime_object), + course_id=activity["course_id"], org_id=activity["org_id"], **activity_object.dict(), ) diff --git a/src/services/dev/dev.py b/src/services/dev/dev.py index 6c1e1879..53a51397 100644 --- a/src/services/dev/dev.py +++ b/src/services/dev/dev.py @@ -7,8 +7,12 @@ def isDevModeEnabled(): if config.general_config.development_mode: return True else: - raise HTTPException( - status_code=403, - detail="Development mode is not enabled", - ) + return False + +def isDevModeEnabledOrRaise(): + config = get_learnhouse_config() + if config.general_config.development_mode: + return True + else: + raise HTTPException(status_code=403, detail="Development mode is not enabled") diff --git a/src/services/install/__init__.py b/src/services/install/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/services/install/install.py b/src/services/install/install.py new file mode 100644 index 00000000..e32b7185 --- /dev/null +++ b/src/services/install/install.py @@ -0,0 +1,475 @@ +from datetime import datetime +from uuid import uuid4 +from fastapi import HTTPException, Request, status +from pydantic import BaseModel +import requests +from config.config import get_learnhouse_config +from src.security.security import security_hash_password +from src.services.courses.activities.activities import Activity, create_activity +from src.services.courses.chapters import create_coursechapter, CourseChapter +from src.services.courses.courses import CourseInDB + +from src.services.orgs.schemas.orgs import Organization, OrganizationInDB +from faker import Faker + + +from src.services.roles.schemas.roles import Elements, Permission, RoleInDB +from src.services.users.schemas.users import ( + PublicUser, + User, + UserInDB, + UserOrganization, + UserRolesInOrganization, + UserWithPassword, +) + + +class InstallInstance(BaseModel): + install_id: str + created_date: str + updated_date: str + step: int + data: dict + + +async def isInstallModeEnabled(): + config = get_learnhouse_config() + + if config.general_config.install_mode: + return True + else: + raise HTTPException( + status_code=403, + detail="Install mode is not enabled", + ) + + +async def create_install_instance(request: Request, data: dict): + installs = request.app.db["installs"] + + # get install_id + install_id = str(f"install_{uuid4()}") + created_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + updated_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + step = 1 + + # create install + install = InstallInstance( + install_id=install_id, + created_date=created_date, + updated_date=updated_date, + step=step, + data=data, + ) + + # insert install + installs.insert_one(install.dict()) + + return install + + +async def get_latest_install_instance(request: Request): + installs = request.app.db["installs"] + + # get latest created install instance using find_one + install = await installs.find_one( + sort=[("created_date", -1)], limit=1, projection={"_id": 0} + ) + + if install is None: + raise HTTPException( + status_code=404, + detail="No install instance found", + ) + + else: + install = InstallInstance(**install) + + return install + + +async def update_install_instance(request: Request, data: dict, step: int): + installs = request.app.db["installs"] + + # get latest created install + install = await installs.find_one( + sort=[("created_date", -1)], limit=1, projection={"_id": 0} + ) + + if install is None: + return None + + else: + # update install + install["data"] = data + install["step"] = step + install["updated_date"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # update install + await installs.update_one( + {"install_id": install["install_id"]}, {"$set": install} + ) + + install = InstallInstance(**install) + + return install + + +############################################################################################################ +# Steps +############################################################################################################ + + +# Install Default roles +async def install_default_elements(request: Request, data: dict): + roles = request.app.db["roles"] + + # check if default roles ADMIN_ROLE and USER_ROLE already exist + admin_role = await roles.find_one({"role_id": "role_super_admin"}) + user_role = await roles.find_one({"role_id": "role_user"}) + + if admin_role is not None or user_role is not None: + raise HTTPException( + status_code=400, + detail="Default roles already exist", + ) + + # get default roles + SUPER_ADMIN_ROLE = RoleInDB( + name="SuperAdmin Role", + description="This role grants all permissions to the user", + elements=Elements( + courses=Permission( + action_create=True, + action_read=True, + action_update=True, + action_delete=True, + ), + users=Permission( + action_create=True, + action_read=True, + action_update=True, + action_delete=True, + ), + houses=Permission( + action_create=True, + action_read=True, + action_update=True, + action_delete=True, + ), + collections=Permission( + action_create=True, + action_read=True, + action_update=True, + action_delete=True, + ), + organizations=Permission( + action_create=True, + action_read=True, + action_update=True, + action_delete=True, + ), + coursechapters=Permission( + action_create=True, + action_read=True, + action_update=True, + action_delete=True, + ), + activities=Permission( + action_create=True, + action_read=True, + action_update=True, + action_delete=True, + ), + ), + org_id="*", + role_id="role_super_admin", + created_at=str(datetime.now()), + updated_at=str(datetime.now()), + ) + + ADMIN_ROLE = RoleInDB( + name="SuperAdmin Role", + description="This role grants all permissions to the user", + elements=Elements( + courses=Permission( + action_create=True, + action_read=True, + action_update=True, + action_delete=True, + ), + users=Permission( + action_create=True, + action_read=True, + action_update=True, + action_delete=True, + ), + houses=Permission( + action_create=True, + action_read=True, + action_update=True, + action_delete=True, + ), + collections=Permission( + action_create=True, + action_read=True, + action_update=True, + action_delete=True, + ), + organizations=Permission( + action_create=True, + action_read=True, + action_update=True, + action_delete=True, + ), + coursechapters=Permission( + action_create=True, + action_read=True, + action_update=True, + action_delete=True, + ), + activities=Permission( + action_create=True, + action_read=True, + action_update=True, + action_delete=True, + ), + ), + org_id="*", + role_id="role_super_admin", + created_at=str(datetime.now()), + updated_at=str(datetime.now()), + ) + + USER_ROLE = RoleInDB( + name="User role", + description="This role grants read-only permissions to the user", + elements=Elements( + courses=Permission( + action_create=False, + action_read=True, + action_update=False, + action_delete=False, + ), + users=Permission( + action_create=False, + action_read=True, + action_update=False, + action_delete=False, + ), + houses=Permission( + action_create=False, + action_read=True, + action_update=False, + action_delete=False, + ), + collections=Permission( + action_create=False, + action_read=True, + action_update=False, + action_delete=False, + ), + organizations=Permission( + action_create=False, + action_read=True, + action_update=False, + action_delete=False, + ), + coursechapters=Permission( + action_create=False, + action_read=True, + action_update=False, + action_delete=False, + ), + activities=Permission( + action_create=False, + action_read=True, + action_update=False, + action_delete=False, + ), + ), + org_id="*", + role_id="role_user", + created_at=str(datetime.now()), + updated_at=str(datetime.now()), + ) + + try: + # insert default roles + await roles.insert_many( + [ADMIN_ROLE.dict(), USER_ROLE.dict(), SUPER_ADMIN_ROLE.dict()] + ) + return True + + except Exception: + raise HTTPException( + status_code=400, + detail="Error while inserting default roles", + ) + + +# Organization creation +async def install_create_organization( + request: Request, + org_object: Organization, +): + orgs = request.app.db["organizations"] + request.app.db["users"] + + # find if org already exists using name + + isOrgAvailable = await orgs.find_one({"slug": org_object.slug.lower()}) + + if isOrgAvailable: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Organization slug already exists", + ) + + # generate org_id with uuid4 + org_id = str(f"org_{uuid4()}") + + org = OrganizationInDB(org_id=org_id, **org_object.dict()) + + org_in_db = await orgs.insert_one(org.dict()) + + if not org_in_db: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Unavailable database", + ) + + return org.dict() + + +async def install_create_organization_user( + request: Request, user_object: UserWithPassword, org_slug: str +): + users = request.app.db["users"] + + isUsernameAvailable = await users.find_one({"username": user_object.username}) + isEmailAvailable = await users.find_one({"email": user_object.email}) + + if isUsernameAvailable: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Username already exists" + ) + + if isEmailAvailable: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Email already exists" + ) + + # Generate user_id with uuid4 + user_id = str(f"user_{uuid4()}") + + # Set the username & hash the password + user_object.username = user_object.username.lower() + user_object.password = await security_hash_password(user_object.password) + + # Get org_id from org_slug + orgs = request.app.db["organizations"] + + # Check if the org exists + isOrgExists = await orgs.find_one({"slug": org_slug}) + + # If the org does not exist, raise an error + if not isOrgExists: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="You are trying to create a user in an organization that does not exist", + ) + + org_id = isOrgExists["org_id"] + + # Create initial orgs list with the org_id passed in + orgs = [UserOrganization(org_id=org_id, org_role="owner")] + + # Give role + roles = [UserRolesInOrganization(role_id="role_super_admin", org_id=org_id)] + + # Create the user + user = UserInDB( + user_id=user_id, + creation_date=str(datetime.now()), + update_date=str(datetime.now()), + orgs=orgs, + roles=roles, + **user_object.dict(), + ) + + # Insert the user into the database + await users.insert_one(user.dict()) + + return User(**user.dict()) + + +async def create_sample_data(org_slug: str, username: str, request: Request): + Faker(["en_US"]) + fake_multilang = Faker( + ["en_US", "de_DE", "ja_JP", "es_ES", "it_IT", "pt_BR", "ar_PS"] + ) + + users = request.app.db["users"] + orgs = request.app.db["organizations"] + user = await users.find_one({"username": username}) + org = await orgs.find_one({"slug": org_slug.lower()}) + user_id = user["user_id"] + org_id = org["org_id"] + + current_user = PublicUser(**user) + + print(current_user) + for i in range(0, 5): + # get image in BinaryIO format from unsplash and save it to disk + image = requests.get("https://source.unsplash.com/random/800x600") + with open("thumbnail.jpg", "wb") as f: + f.write(image.content) + + course_id = f"course_{uuid4()}" + course = CourseInDB( + name=fake_multilang.unique.sentence(), + description=fake_multilang.unique.text(), + mini_description=fake_multilang.unique.text(), + thumbnail="thumbnail", + org_id=org_id, + learnings=[fake_multilang.unique.sentence() for i in range(0, 5)], + public=True, + chapters=[], + course_id=course_id, + creationDate=str(datetime.now()), + updateDate=str(datetime.now()), + authors=[user_id], + chapters_content=[], + ) + + courses = request.app.db["courses"] + + course = CourseInDB(**course.dict()) + await courses.insert_one(course.dict()) + + # create chapters + for i in range(0, 5): + coursechapter = CourseChapter( + name=fake_multilang.unique.sentence(), + description=fake_multilang.unique.text(), + activities=[], + ) + coursechapter = await create_coursechapter( + request, coursechapter, course_id, current_user + ) + if coursechapter: + # create activities + for i in range(0, 5): + activity = Activity( + name=fake_multilang.unique.sentence(), + type="dynamic", + content={}, + ) + activity = await create_activity( + request, + activity, + org_id, + coursechapter["coursechapter_id"], + current_user, + ) diff --git a/src/services/trail/__init__.py b/src/services/trail/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/services/trail.py b/src/services/trail/trail.py similarity index 89% rename from src/services/trail.py rename to src/services/trail/trail.py index 6c133b3c..08e38700 100644 --- a/src/services/trail.py +++ b/src/services/trail/trail.py @@ -121,23 +121,19 @@ async def get_user_trail_with_orgslug( if not trail: return Trail(masked=False, courses=[]) - # Check if these courses still exist in the database + course_ids = [course["course_id"] for course in trail["courses"]] + + live_courses = await courses_mongo.find({"course_id": {"$in": course_ids}}).to_list( + length=None + ) + for course in trail["courses"]: course_id = course["course_id"] - course_object = await courses_mongo.find_one( - {"course_id": course_id}, {"_id": 0} - ) - print('checking course ' + course_id) - if not course_object: - print("Course not found " + course_id) - trail["courses"].remove(course) + + if course_id not in [course["course_id"] for course in live_courses]: + course["masked"] = True continue - course["course_object"] = course_object - - for courses in trail["courses"]: - course_id = courses["course_id"] - chapters_meta = await get_coursechapters_meta(request, course_id, user) activities = chapters_meta["activities"] @@ -146,11 +142,11 @@ async def get_user_trail_with_orgslug( {"course_id": course_id}, {"_id": 0} ) - courses["course_object"] = course_object + course["course_object"] = course_object num_activities = len(activities) - num_completed_activities = len(courses.get("activities_marked_complete", [])) - courses["progress"] = ( + num_completed_activities = len(course.get("activities_marked_complete", [])) + course["progress"] = ( round((num_completed_activities / num_activities) * 100, 2) if num_activities > 0 else 0 @@ -176,6 +172,12 @@ async def add_activity_to_trail( {"user_id": user.user_id, "courses.course_id": courseid, "org_id": org_id} ) + if user.user_id == "anonymous": + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Anonymous users cannot add activity to trail", + ) + if not trail: return Trail(masked=False, courses=[]) @@ -206,6 +208,12 @@ async def add_course_to_trail( trails = request.app.db["trails"] orgs = request.app.db["organizations"] + if user.user_id == "anonymous": + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Anonymous users cannot add activity to trail", + ) + org = await orgs.find_one({"slug": orgslug}) org = PublicOrganization(**org) @@ -251,12 +259,15 @@ async def remove_course_from_trail( trails = request.app.db["trails"] orgs = request.app.db["organizations"] + if user.user_id == "anonymous": + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Anonymous users cannot add activity to trail", + ) + org = await orgs.find_one({"slug": orgslug}) org = PublicOrganization(**org) - - print(org) - trail = await trails.find_one({"user_id": user.user_id, "org_id": org["org_id"]}) if not trail: diff --git a/src/services/utils/upload_content.py b/src/services/utils/upload_content.py index 81d766a2..0776fe94 100644 --- a/src/services/utils/upload_content.py +++ b/src/services/utils/upload_content.py @@ -30,6 +30,7 @@ async def upload_content( elif content_delivery == "s3api": # Upload to server then to s3 (AWS Keys are stored in environment variables and are loaded by boto3) # TODO: Improve implementation of this + print("Uploading to s3...") s3 = boto3.client( "s3", endpoint_url=learnhouse_config.hosting_config.content_delivery.s3api.endpoint_url,