diff --git a/apps/api/src/security/rbac/rbac.py b/apps/api/src/security/rbac/rbac.py index 2e92ff09..d7055a79 100644 --- a/apps/api/src/security/rbac/rbac.py +++ b/apps/api/src/security/rbac/rbac.py @@ -33,19 +33,18 @@ async def authorization_verify_if_element_is_public( detail="User rights : You don't have the right to perform this action", ) - if element_nature == "collections" and action == "read": - - statement = select(Collection).where( - Collection.public == True, Collection.collection_uuid == element_uuid + if element_nature == "collections" and action == "read": + statement = select(Collection).where( + Collection.public == True, Collection.collection_uuid == element_uuid + ) + collection = db_session.exec(statement).first() + if collection: + return True + else: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="User rights : You don't have the right to perform this action", ) - collection = db_session.exec(statement).first() - if collection: - return True - else: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="User rights : You don't have the right to perform this action", - ) else: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, diff --git a/apps/api/src/services/courses/collections.py b/apps/api/src/services/courses/collections.py index 83f976a7..ac9627b6 100644 --- a/apps/api/src/services/courses/collections.py +++ b/apps/api/src/services/courses/collections.py @@ -28,7 +28,7 @@ from fastapi import HTTPException, status, Request async def get_collection( request: Request, collection_uuid: str, - current_user: PublicUser, + current_user: PublicUser | AnonymousUser, db_session: Session, ) -> CollectionRead: statement = select(Collection).where(Collection.collection_uuid == collection_uuid) @@ -48,6 +48,7 @@ async def get_collection( statement_all = ( select(Course) .join(CollectionCourse, Course.id == CollectionCourse.course_id) + .where(CollectionCourse.org_id == collection.org_id) .distinct(Course.id) ) @@ -57,7 +58,7 @@ async def get_collection( .where(CollectionCourse.org_id == collection.org_id, Course.public == True) ) - if current_user.id == 0: + if current_user.user_uuid == "user_anonymous": statement = statement_public else: statement = statement_all @@ -88,7 +89,6 @@ async def create_collection( # Add collection to database db_session.add(collection) db_session.commit() - db_session.refresh(collection) # Link courses to collection @@ -184,6 +184,7 @@ async def update_collection( statement = ( select(Course) .join(CollectionCourse, Course.id == CollectionCourse.course_id) + .where(Course.org_id == collection.org_id) .distinct(Course.id) ) @@ -255,6 +256,7 @@ async def get_collections( statement_all = ( select(Course) .join(CollectionCourse, Course.id == CollectionCourse.course_id) + .where(CollectionCourse.org_id == collection.org_id) .distinct(Course.id) ) statement_public = ( @@ -297,8 +299,10 @@ async def rbac_check( detail="User rights : You are not allowed to read this collection", ) else: - res = await authorization_verify_based_on_roles_and_authorship_and_usergroups( - request, current_user.id, action, collection_uuid, db_session + res = ( + await authorization_verify_based_on_roles_and_authorship_and_usergroups( + request, current_user.id, action, collection_uuid, db_session + ) ) return res else: diff --git a/apps/web/app/auth/forgot/forgot.tsx b/apps/web/app/auth/forgot/forgot.tsx index 8b3cb7c2..ecfce06e 100644 --- a/apps/web/app/auth/forgot/forgot.tsx +++ b/apps/web/app/auth/forgot/forgot.tsx @@ -42,6 +42,7 @@ function ForgotPasswordClient() { email: '' }, validate, + validateOnBlur: true, onSubmit: async (values) => { setIsSubmitting(true) let res = await sendResetLink(values.email, org?.id) diff --git a/apps/web/app/auth/login/login.tsx b/apps/web/app/auth/login/login.tsx index e766cf51..49dd7161 100644 --- a/apps/web/app/auth/login/login.tsx +++ b/apps/web/app/auth/login/login.tsx @@ -51,8 +51,17 @@ const LoginClient = (props: LoginClientProps) => { password: '', }, validate, - onSubmit: async (values) => { + validateOnBlur: true, + validateOnChange: true, + onSubmit: async (values, {validateForm, setErrors, setSubmitting}) => { setIsSubmitting(true) + const errors = await validateForm(values); + if (Object.keys(errors).length > 0) { + setErrors(errors); + setSubmitting(false); + return; + } + const res = await signIn('credentials', { redirect: false, email: values.email, @@ -139,7 +148,7 @@ const LoginClient = (props: LoginClientProps) => { onChange={formik.handleChange} value={formik.values.email} type="email" - required + /> @@ -155,7 +164,7 @@ const LoginClient = (props: LoginClientProps) => { onChange={formik.handleChange} value={formik.values.password} type="password" - required + /> @@ -170,7 +179,7 @@ const LoginClient = (props: LoginClientProps) => {
- diff --git a/apps/web/app/editor/course/[courseid]/activity/[activityuuid]/edit/page.tsx b/apps/web/app/editor/course/[courseid]/activity/[activityuuid]/edit/page.tsx index 72183f3a..4dca9687 100644 --- a/apps/web/app/editor/course/[courseid]/activity/[activityuuid]/edit/page.tsx +++ b/apps/web/app/editor/course/[courseid]/activity/[activityuuid]/edit/page.tsx @@ -49,6 +49,7 @@ const EditActivity = async (params: any) => { { revalidate: 0, tags: ['activities'] }, access_token ? access_token : null ) + const org = await getOrganizationContextInfoWithId(courseInfo.org_id, { revalidate: 180, tags: ['organizations'], diff --git a/apps/web/app/orgs/[orgslug]/dash/page.tsx b/apps/web/app/orgs/[orgslug]/dash/page.tsx index 9f104e12..acc01033 100644 --- a/apps/web/app/orgs/[orgslug]/dash/page.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/page.tsx @@ -1,7 +1,7 @@ import Image from 'next/image' import React from 'react' import learnhousetextlogo from '../../../../public/learnhouse_logo.png' -import { BookCopy, School, Settings, Users } from 'lucide-react' +import { BookCopy, School, Settings, University, Users } from 'lucide-react' import Link from 'next/link' import AdminAuthorization from '@components/Security/AdminAuthorization' @@ -62,12 +62,13 @@ function DashboardHome() {
- +
- Learn LearnHouse + LearnHouse University
diff --git a/apps/web/app/orgs/[orgslug]/layout.tsx b/apps/web/app/orgs/[orgslug]/layout.tsx index d722fb9f..de924c3d 100644 --- a/apps/web/app/orgs/[orgslug]/layout.tsx +++ b/apps/web/app/orgs/[orgslug]/layout.tsx @@ -1,5 +1,6 @@ 'use client' import { OrgProvider } from '@components/Contexts/OrgContext' +import NextTopLoader from 'nextjs-toploader'; import Toast from '@components/StyledElements/Toast/Toast' import '@styles/globals.css' @@ -13,6 +14,7 @@ export default function RootLayout({ return (
+ {children} diff --git a/apps/web/components/Contexts/CourseContext.tsx b/apps/web/components/Contexts/CourseContext.tsx index 11d7d5df..f407cd00 100644 --- a/apps/web/components/Contexts/CourseContext.tsx +++ b/apps/web/components/Contexts/CourseContext.tsx @@ -4,6 +4,7 @@ import { swrFetcher } from '@services/utils/ts/requests' import React, { createContext, useContext, useEffect, useReducer } from 'react' import useSWR from 'swr' import { useLHSession } from '@components/Contexts/LHSessionContext' +import PageLoading from '@components/Objects/Loaders/PageLoading' export const CourseContext = createContext(null) export const CourseDispatchContext = createContext(null) @@ -33,15 +34,17 @@ export function CourseProvider({ children, courseuuid }: any) { }, [courseStructureData]); if (error) return
Failed to load course structure
; - if (!courseStructureData) return
Loading...
; + if (!courseStructureData) return ''; - return ( - - - {children} - - - ) + if (courseStructureData) { + return ( + + + {children} + + + ) + } } export function useCourse() { diff --git a/apps/web/components/Contexts/OrgContext.tsx b/apps/web/components/Contexts/OrgContext.tsx index f434b3f1..d9482fed 100644 --- a/apps/web/components/Contexts/OrgContext.tsx +++ b/apps/web/components/Contexts/OrgContext.tsx @@ -30,7 +30,7 @@ export function OrgProvider({ children, orgslug }: { children: React.ReactNode, const isUserPartOfTheOrg = useMemo(() => orgs?.some((userOrg: any) => userOrg.id === org?.id), [orgs, org?.id]) if (orgError || orgsError) return - if (!org || !orgs || !session) return
Loading...
+ if (!org || !orgs || !session) return
if (!isOrgActive) return if (!isUserPartOfTheOrg && session.status == 'authenticated' && !isAllowedPathname) { return ( diff --git a/apps/web/components/Dashboard/Course/EditCourseAccess/EditCourseAccess.tsx b/apps/web/components/Dashboard/Course/EditCourseAccess/EditCourseAccess.tsx index 38ec8b3c..eac3c8db 100644 --- a/apps/web/components/Dashboard/Course/EditCourseAccess/EditCourseAccess.tsx +++ b/apps/web/components/Dashboard/Course/EditCourseAccess/EditCourseAccess.tsx @@ -7,7 +7,7 @@ import { unLinkResourcesToUserGroup } from '@services/usergroups/usergroups' import { swrFetcher } from '@services/utils/ts/requests' import { Globe, SquareUserRound, Users, X } from 'lucide-react' import { useLHSession } from '@components/Contexts/LHSessionContext' -import React from 'react' +import React, { useEffect, useState } from 'react' import toast from 'react-hot-toast' import useSWR, { mutate } from 'swr' @@ -17,132 +17,132 @@ type EditCourseAccessProps = { } function EditCourseAccess(props: EditCourseAccessProps) { - const [error, setError] = React.useState('') const session = useLHSession() as any; const access_token = session?.data?.tokens?.access_token; - const course = useCourse() as any; const { isLoading, courseStructure } = course as any; - const dispatchCourse = useCourseDispatch() as any - const { data: usergroups } = useSWR( - courseStructure ? `${getAPIUrl()}usergroups/resource/${courseStructure.course_uuid}` : null, - (url) => swrFetcher(url, access_token) - ) - const [isPublic, setIsPublic] = React.useState(courseStructure.public) + const dispatchCourse = useCourseDispatch() as any; + const { data: usergroups } = useSWR(courseStructure ? `${getAPIUrl()}usergroups/resource/${courseStructure.course_uuid}` : null, (url) => swrFetcher(url, access_token)); + const [isClientPublic, setIsClientPublic] = useState(undefined); - React.useEffect(() => { - // This code will run whenever form values are updated - if ((isPublic !== courseStructure.public) && isLoading) { - dispatchCourse({ type: 'setIsNotSaved' }) - const updatedCourse = { - ...courseStructure, - public: isPublic, - } - dispatchCourse({ type: 'setCourseStructure', payload: updatedCourse }) + useEffect(() => { + if (!isLoading && courseStructure?.public !== undefined) { + setIsClientPublic(courseStructure.public); } - }, [course, isPublic]) + }, [isLoading, courseStructure]); + + useEffect(() => { + if (!isLoading && courseStructure?.public !== undefined && isClientPublic !== undefined) { + if (isClientPublic !== courseStructure.public) { + dispatchCourse({ type: 'setIsNotSaved' }); + const updatedCourse = { + ...courseStructure, + public: isClientPublic, + }; + dispatchCourse({ type: 'setCourseStructure', payload: updatedCourse }); + } + } + }, [isLoading, isClientPublic, courseStructure, dispatchCourse]); + return (
- {' '} -
-
-
-

Access to the course

-

- {' '} - Choose if want your course to be publicly available on the internet or only accessible to signed in users{' '} -

+ {courseStructure && ( +
+
+
+
+

Access to the course

+

+ Choose if you want your course to be publicly available on the internet or only accessible to signed in users +

+
+
+ + {isClientPublic && ( +
+ Active +
+ )} +
+ +
+ Public +
+
+ The Course is publicly available on the internet, it is indexed by search engines and can be accessed by anyone +
+
+
+ } + functionToExecute={() => setIsClientPublic(true)} + status="info" + /> + + {!isClientPublic && ( +
+ Active +
+ )} +
+ +
+ Users Only +
+
+ The Course is only accessible to signed in users, additionally you can choose which UserGroups can access this course +
+
+
+ } + functionToExecute={() => setIsClientPublic(false)} + status="info" + /> +
+ {!isClientPublic && } +
-
- - {isPublic ? ( -
- Active -
- ) : null} -
- -
- Public -
-
- The Course is publicly available on the internet, it is indexed by search engines and can be accessed by anyone -
-
- -
- } - functionToExecute={() => { - setIsPublic(true) - }} - status="info" - > - - {!isPublic ? ( -
- Active -
- ) : null} -
- -
- Users Only -
-
- The Course is only accessible to signed in users, additionaly you can choose which UserGroups can access this course -
-
- -
- } - functionToExecute={() => { - setIsPublic(false) - }} - status="info" - > -
- {!isPublic ? () : null} -
+ )} - ) + ); } - function UserGroupsSection({ usergroups }: { usergroups: any[] }) { - const course = useCourse() as any - const [userGroupModal, setUserGroupModal] = React.useState(false) + const course = useCourse() as any; + const [userGroupModal, setUserGroupModal] = useState(false); const session = useLHSession() as any; const access_token = session?.data?.tokens?.access_token; const removeUserGroupLink = async (usergroup_id: number) => { - const res = await unLinkResourcesToUserGroup(usergroup_id, course.courseStructure.course_uuid, access_token) - if (res.status === 200) { - toast.success('Successfully unliked from usergroup') - mutate(`${getAPIUrl()}usergroups/resource/${course.courseStructure.course_uuid}`) + try { + const res = await unLinkResourcesToUserGroup(usergroup_id, course.courseStructure.course_uuid, access_token); + if (res.status === 200) { + toast.success('Successfully unlinked from usergroup'); + mutate(`${getAPIUrl()}usergroups/resource/${course.courseStructure.course_uuid}`); + } else { + toast.error(`Error ${res.status}: ${res.data.detail}`); + } + } catch (error) { + toast.error('An error occurred while unlinking the user group.'); } - else { - toast.error('Error ' + res.status + ': ' + res.data.detail) - } - } + }; return ( <> -
+

UserGroups

- {' '} - You can choose to give access to this course to specific groups of users only by linking it to a UserGroup{' '} + You can choose to give access to this course to specific groups of users only by linking it to a UserGroup

@@ -152,67 +152,48 @@ function UserGroupsSection({ usergroups }: { usergroups: any[] }) { - <> - - {usergroups?.map((usergroup: any) => ( - - - - - ))} - - + + {usergroups?.map((usergroup: any) => ( + + + + + ))} +
Actions
{usergroup.name} - - - Delete link - - } - functionToExecute={() => { - removeUserGroupLink(usergroup.id) - }} - status="warning" - > -
{usergroup.name} + + + Delete link + + } + functionToExecute={() => removeUserGroupLink(usergroup.id)} + status="warning" + /> +
-
+
- setUserGroupModal(!userGroupModal) - } + isDialogOpen={userGroupModal} + onOpenChange={() => setUserGroupModal(!userGroupModal)} minHeight="no-min" - minWidth='md' - dialogContent={ - - - } + minWidth="md" + dialogContent={} dialogTitle="Link Course to a UserGroup" - dialogDescription={ - 'Choose a UserGroup to link this course to, Users from this UserGroup will have access to this course.' - } + dialogDescription="Choose a UserGroup to link this course to. Users from this UserGroup will have access to this course." dialogTrigger={ - } /> -
- ) + ); } -export default EditCourseAccess \ No newline at end of file +export default EditCourseAccess; diff --git a/apps/web/components/Dashboard/Course/EditCourseStructure/DraggableElements/ActivityElement.tsx b/apps/web/components/Dashboard/Course/EditCourseStructure/DraggableElements/ActivityElement.tsx index d001fa06..a76c72e7 100644 --- a/apps/web/components/Dashboard/Course/EditCourseStructure/DraggableElements/ActivityElement.tsx +++ b/apps/web/components/Dashboard/Course/EditCourseStructure/DraggableElements/ActivityElement.tsx @@ -5,6 +5,7 @@ import { revalidateTags } from '@services/utils/ts/requests' import { Eye, File, + FilePenLine, MoreVertical, Pencil, Save, @@ -44,7 +45,7 @@ function ActivityElement(props: ActivitiyElementProps) { const activityUUID = props.activity.activity_uuid async function deleteActivityUI() { - await deleteActivity(props.activity.activity_uuid,access_token) + await deleteActivity(props.activity.activity_uuid, access_token) mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`) await revalidateTags(['courses'], props.orgslug) router.refresh() @@ -63,7 +64,7 @@ function ActivityElement(props: ActivitiyElementProps) { content: props.activity.content, } - await updateActivity(modifiedActivityCopy, activityUUID,access_token) + await updateActivity(modifiedActivityCopy, activityUUID, access_token) mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`) await revalidateTags(['courses'], props.orgslug) router.refresh() @@ -141,10 +142,13 @@ function ActivityElement(props: ActivitiyElementProps) { '' )}/edit` } + prefetch className=" hover:cursor-pointer p-1 px-3 bg-sky-700 rounded-md items-center" - rel="noopener noreferrer" + target='_blank' // hotfix for an editor prosemirror bug > -
Edit
+
+ Edit Page +
)} @@ -159,10 +163,12 @@ function ActivityElement(props: ActivitiyElementProps) { '' )}` } - className=" hover:cursor-pointer p-1 px-3 bg-gray-200 rounded-md" + prefetch + className=" hover:cursor-pointer p-1 px-3 bg-gray-200 rounded-md font-bold text-xs flex items-center space-x-1" rel="noopener noreferrer" > - + + Preview
{/* Delete Button */} diff --git a/apps/web/components/Objects/Editor/Editor.tsx b/apps/web/components/Objects/Editor/Editor.tsx index d66d92d5..185c8907 100644 --- a/apps/web/components/Objects/Editor/Editor.tsx +++ b/apps/web/components/Objects/Editor/Editor.tsx @@ -47,6 +47,7 @@ import useGetAIFeatures from '@components/AI/Hooks/useGetAIFeatures' import Collaboration from '@tiptap/extension-collaboration' import CollaborationCursor from '@tiptap/extension-collaboration-cursor' import ActiveAvatars from './ActiveAvatars' +import { getUriWithOrg } from '@services/config/config' interface Editor { content: string @@ -182,11 +183,11 @@ function Editor(props: Editor) { diff --git a/apps/web/components/Objects/Editor/EditorWrapper.tsx b/apps/web/components/Objects/Editor/EditorWrapper.tsx index aa0c2364..6353a297 100644 --- a/apps/web/components/Objects/Editor/EditorWrapper.tsx +++ b/apps/web/components/Objects/Editor/EditorWrapper.tsx @@ -25,7 +25,7 @@ interface EditorWrapperProps { function EditorWrapper(props: EditorWrapperProps): JSX.Element { const session = useLHSession() as any - const access_token = session?.data?.tokens?.access_token; + const access_token = session?.data?.tokens?.access_token; // Define provider in the state const [provider, setProvider] = React.useState(null); const [thisPageColor, setThisPageColor] = useState(randomColor({ luminosity: 'light' }) as string) @@ -51,7 +51,7 @@ function EditorWrapper(props: EditorWrapperProps): JSX.Element { }, 10); }; - + // Store the Y document in the browser new IndexeddbPersistence(props.activity.activity_uuid, doc) @@ -80,7 +80,7 @@ function EditorWrapper(props: EditorWrapperProps): JSX.Element { } }); - toast.promise(updateActivity(activity, activity.activity_uuid,access_token), { + toast.promise(updateActivity(activity, activity.activity_uuid, access_token), { loading: 'Saving...', success: Activity saved!, error: Could not save., diff --git a/apps/web/components/Objects/Menu/MenuLinks.tsx b/apps/web/components/Objects/Menu/MenuLinks.tsx index d8fa3ebe..3566ff34 100644 --- a/apps/web/components/Objects/Menu/MenuLinks.tsx +++ b/apps/web/components/Objects/Menu/MenuLinks.tsx @@ -1,12 +1,13 @@ import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement' import { getUriWithOrg } from '@services/config/config' +import { BookCopy, Signpost, SquareLibrary } from 'lucide-react' import Link from 'next/link' import React from 'react' function MenuLinks(props: { orgslug: string }) { return ( -
-
    +
    +
      { const orgslug = props.orgslug return ( -
    • +
    • {props.type == 'courses' && ( <> - - - + {' '} Courses )} {props.type == 'collections' && ( <> - - - + {' '} Collections )} {props.type == 'trail' && ( <> - - - + {' '} Trail )} diff --git a/apps/web/components/Objects/Modals/Activities/Create/NewActivityModal/DynamicCanva.tsx b/apps/web/components/Objects/Modals/Activities/Create/NewActivityModal/DynamicCanva.tsx index faff1760..7d6082ab 100644 --- a/apps/web/components/Objects/Modals/Activities/Create/NewActivityModal/DynamicCanva.tsx +++ b/apps/web/components/Objects/Modals/Activities/Create/NewActivityModal/DynamicCanva.tsx @@ -59,7 +59,7 @@ function DynamicCanvaModal({ submitActivity, chapterId, course }: any) { -