diff --git a/front/app/orgs/[orgslug]/(withmenu)/collections/admin.tsx b/front/app/orgs/[orgslug]/(withmenu)/collections/admin.tsx index 2af0c9bc..e32eb804 100644 --- a/front/app/orgs/[orgslug]/(withmenu)/collections/admin.tsx +++ b/front/app/orgs/[orgslug]/(withmenu)/collections/admin.tsx @@ -14,13 +14,13 @@ const CollectionAdminEditsArea = (props: any) => { const deleteCollectionUI = async (collectionId: number) => { await deleteCollection(collectionId); - revalidateTags(["collections"]); + revalidateTags(["collections"], props.orgslug); // reload the page router.refresh(); router.push(getUriWithOrg(props.orgslug, "/collections")); // refresh page (FIX for Next.js BUG) - window.location.reload(); + //window.location.reload(); } return ( diff --git a/front/app/orgs/[orgslug]/(withmenu)/collections/new/page.tsx b/front/app/orgs/[orgslug]/(withmenu)/collections/new/page.tsx index a440348b..41f05ff0 100644 --- a/front/app/orgs/[orgslug]/(withmenu)/collections/new/page.tsx +++ b/front/app/orgs/[orgslug]/(withmenu)/collections/new/page.tsx @@ -43,13 +43,11 @@ function NewCollection(params: any) { org_id: org.org_id, }; await createCollection(collection); - revalidateTags(["collections"]); + revalidateTags(["collections"], orgslug); router.prefetch(getUriWithOrg(orgslug, "/collections")); router.push(getUriWithOrg(orgslug, "/collections")); router.refresh(); - // refresh page (FIX for Next.js BUG) - window.location.reload(); }; 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 e9f94228..b2bee0a7 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 @@ -10,6 +10,7 @@ import DocumentPdfActivity from "@components/Pages/Activities/DocumentPdf/Docume import ActivityIndicators from "@components/Pages/Courses/ActivityIndicators"; import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper"; import { useRouter } from "next/navigation"; +import AuthenticatedClientElement from "@components/Security/AuthenticatedClientElement"; interface ActivityClientProps { activityid: string; @@ -64,8 +65,10 @@ function ActivityClient(props: ActivityClientProps) {

{activity.name}

- + + +
diff --git a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx index adf51412..97958e52 100644 --- a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx +++ b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx @@ -22,7 +22,7 @@ const CourseClient = (props: any) => { async function startCourseUI() { // Create activity await startCourse("course_" + courseid, orgslug); - revalidateTags(['courses']); + revalidateTags(['courses'], orgslug); router.refresh(); // refresh page (FIX for Next.js BUG) @@ -33,7 +33,7 @@ const CourseClient = (props: any) => { // Close activity let activity = await removeCourse("course_" + courseid, orgslug); // Mutate course - revalidateTags(['courses']); + revalidateTags(['courses'], orgslug); router.refresh(); // refresh page (FIX for Next.js BUG) diff --git a/front/app/orgs/[orgslug]/(withmenu)/courses/courses.tsx b/front/app/orgs/[orgslug]/(withmenu)/courses/courses.tsx index 1257e34b..e20dd860 100644 --- a/front/app/orgs/[orgslug]/(withmenu)/courses/courses.tsx +++ b/front/app/orgs/[orgslug]/(withmenu)/courses/courses.tsx @@ -34,7 +34,7 @@ function Courses(props: CourseProps) { async function deleteCourses(course_id: any) { await deleteCourseFromBackend(course_id); - revalidateTags(['courses']); + revalidateTags(['courses'], orgslug); router.refresh(); } diff --git a/front/app/orgs/[orgslug]/login/page.tsx b/front/app/orgs/[orgslug]/login/page.tsx index aaad485c..08aa23a7 100644 --- a/front/app/orgs/[orgslug]/login/page.tsx +++ b/front/app/orgs/[orgslug]/login/page.tsx @@ -10,8 +10,8 @@ import Toast from '@components/StyledElements/Toast/Toast'; import { toast } from 'react-hot-toast'; const Login = () => { - const [email, setEmail] = React.useState("admin@admin.admin"); - const [password, setPassword] = React.useState("admin"); + const [email, setEmail] = React.useState(""); + const [password, setPassword] = React.useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const router = useRouter(); @@ -44,13 +44,13 @@ const Login = () => { return (
- + Email - Please provide an email + Please provide an email @@ -59,7 +59,7 @@ const Login = () => { Password - Please provide a password + Please provide a password diff --git a/front/app/orgs/[orgslug]/settings/layout.tsx b/front/app/orgs/[orgslug]/settings/layout.tsx index 10402e16..3e2ff305 100644 --- a/front/app/orgs/[orgslug]/settings/layout.tsx +++ b/front/app/orgs/[orgslug]/settings/layout.tsx @@ -6,38 +6,45 @@ import LearnHouseWhiteLogo from '@public/learnhouse_text_white.png'; import AuthProvider, { AuthContext } from '@components/Security/AuthProvider'; import Avvvatars from 'avvvatars-react'; import Image from 'next/image'; +import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'; +import { getOrganizationContextInfo } from '@services/organizations/orgs'; -function SettingsLayout({ children, params }: { children: React.ReactNode, params: any }) { +async function SettingsLayout({ children, params }: { children: React.ReactNode, params: any }) { const auth: any = React.useContext(AuthContext); + const orgslug = params.orgslug; + + let org = await getOrganizationContextInfo(orgslug, {}); return ( - <> - -
- - - - Learnhouse logo - {auth.isAuthenticated && ( - - )} - - - Account -
    -
  • Profile
  • -
  • Passwords
  • -
- Organization -
    -
  • General
  • -
-
-
- - {children} - -
+ <> + +
+ + + + Learnhouse logo + {auth.isAuthenticated && ( + + )} + + + Account +
    +
  • Profile
  • +
  • Passwords
  • +
+ + Organization +
    +
  • General
  • +
+
+
+
+ + {children} + +
) } @@ -59,7 +66,7 @@ const LeftWrapper = styled('div', { const LeftTopArea = styled('div', { display: 'flex', marginLeft: '20px', - + alignItems: 'center', img: { @@ -72,7 +79,7 @@ const LeftTopArea = styled('div', { placeContent: 'center', } - + }) const LeftMenuWrapper = styled('div', { diff --git a/front/app/orgs/[orgslug]/settings/organization/general/organization.tsx b/front/app/orgs/[orgslug]/settings/organization/general/organization.tsx index 69830d16..116c919b 100644 --- a/front/app/orgs/[orgslug]/settings/organization/general/organization.tsx +++ b/front/app/orgs/[orgslug]/settings/organization/general/organization.tsx @@ -33,12 +33,9 @@ function OrganizationClient(props: any) { let org_id = org.org_id; await uploadOrganizationLogo(org_id, selectedFile); setSelectedFile(null); // Reset the selected file - revalidateTags(['organizations']); + revalidateTags(['organizations'], org.slug); router.refresh(); - // refresh page (FIX for Next.js BUG) - window.location.reload(); - } }; @@ -55,6 +52,10 @@ function OrganizationClient(props: any) { const updateOrg = async (values: OrganizationValues) => { let org_id = org.org_id; await updateOrganization(org_id, values); + + // Mutate the org + revalidateTags(['organizations'], org.slug); + router.refresh(); } diff --git a/front/components/Objects/Modals/Course/Create/CreateCourse.tsx b/front/components/Objects/Modals/Course/Create/CreateCourse.tsx index 5e7c0fc2..e9870b9f 100644 --- a/front/components/Objects/Modals/Course/Create/CreateCourse.tsx +++ b/front/components/Objects/Modals/Course/Create/CreateCourse.tsx @@ -43,7 +43,7 @@ function CreateCourseModal({ closeModal, orgslug }: any) { e.preventDefault(); setIsSubmitting(true); let status = await createNewCourse(orgId, { name, description }, thumbnail); - revalidateTags(['courses']); + revalidateTags(['courses'], orgslug); setIsSubmitting(false); if (status.org_id == orgId) { diff --git a/front/components/Pages/Trail/TrailCourseElement.tsx b/front/components/Pages/Trail/TrailCourseElement.tsx index dd32c5c8..c2d9ec6a 100644 --- a/front/components/Pages/Trail/TrailCourseElement.tsx +++ b/front/components/Pages/Trail/TrailCourseElement.tsx @@ -18,7 +18,7 @@ function TrailCourseElement(props: TrailCourseElementProps) { // Close activity let activity = await removeCourse(course_id, props.orgslug); // Mutate course - revalidateTags(['courses']); + revalidateTags(['courses'], props.orgslug); // Mutate mutate(`${getAPIUrl()}trail/org_slug/${props.orgslug}/trail`); diff --git a/front/components/Security/AuthenticatedClientElement.tsx b/front/components/Security/AuthenticatedClientElement.tsx index 8e442352..6ced1f88 100644 --- a/front/components/Security/AuthenticatedClientElement.tsx +++ b/front/components/Security/AuthenticatedClientElement.tsx @@ -9,7 +9,7 @@ interface AuthenticatedClientElementProps { } -function AuthenticatedClientElement(props: AuthenticatedClientElementProps) { +export const AuthenticatedClientElement = (props: AuthenticatedClientElementProps) => { const auth: any = React.useContext(AuthContext); // Available roles diff --git a/front/services/utils/ts/requests.ts b/front/services/utils/ts/requests.ts index e5b9bf5a..b0b49390 100644 --- a/front/services/utils/ts/requests.ts +++ b/front/services/utils/ts/requests.ts @@ -1,6 +1,6 @@ import { AppRouterInstance } from "next/dist/shared/lib/app-router-context"; import { denyAccessToUser } from "../react/middlewares/views"; -import { LEARNHOUSE_DOMAIN, LEARNHOUSE_HTTP_PROTOCOL } from "@services/config/config"; +import { getUriWithOrg, LEARNHOUSE_DOMAIN, LEARNHOUSE_HTTP_PROTOCOL } from "@services/config/config"; export const RequestBody = (method: string, data: any, next: any) => { let HeadersConfig = new Headers({ "Content-Type": "application/json" }); @@ -78,15 +78,16 @@ export const swrFetcher = async (url: string, body: any, router?: AppRouterInsta export const errorHandling = (res: any) => { if (!res.ok) { - const error: any = new Error(`${res.status}: ${res.statusText}`, {}); + const error: any = new Error(`${res.statusText}`); error.status = res.status; throw error; } return res.json(); }; -export const revalidateTags = (tags: string[]) => { +export const revalidateTags = (tags: string[], orgslug: string) => { + const url = getUriWithOrg(orgslug, ""); tags.forEach((tag) => { - fetch(`${LEARNHOUSE_HTTP_PROTOCOL}${LEARNHOUSE_DOMAIN}/api/revalidate?tag=${tag}`); + fetch(`${url}/api/revalidate?tag=${tag}`); }); }; diff --git a/src/security/security.py b/src/security/security.py index f6384b09..8a69005d 100644 --- a/src/security/security.py +++ b/src/security/security.py @@ -18,6 +18,7 @@ ALGORITHM = "HS256" ### 🔒 Passwords Hashing ############################################################## + async def security_hash_password(password: str): return pbkdf2_sha256.hash(password) @@ -25,12 +26,15 @@ async def security_hash_password(password: str): async def security_verify_password(plain_password: str, hashed_password: str): return pbkdf2_sha256.verify(plain_password, hashed_password) + ### 🔒 Passwords Hashing ############################################################## ### 🔒 Roles checking ############################################################## -async def verify_user_rights_with_roles(request: Request, action: str, user_id: str, element_id: str, element_org_id: str): +async def verify_user_rights_with_roles( + request: Request, action: str, user_id: str, element_id: str, element_org_id: str +): """ Check if the user has the right to perform the action on the element """ @@ -39,49 +43,45 @@ async def verify_user_rights_with_roles(request: Request, action: str, user_id: user = await users.find_one({"user_id": user_id}) - # Check if user is available + ######### + # Users existence verification + ######### + if not user and user_id != "anonymous": raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="User not found") - + status_code=status.HTTP_404_NOT_FOUND, detail="User rights : User not found" + ) + # Check if user is anonymous if user_id == "anonymous": return False - # Check if the user is an admin + # Get User user: UserInDB = UserInDB(**await users.find_one({"user_id": user_id})) - # Organization roles verification + ######### + # Organization Roles verification + ######### + for org in user.orgs: - # TODO: Check if the org_id (user) is the same as the org_id (element) - if org.org_id == element_org_id: - return True + # Check if user is owner or reader of the organization + if org.org_role == ("owner" or "editor"): + return True - # Check if user is owner or reader of the organization - if org.org_role == ("owner" or "editor"): - return True - - # If the user is not an owner or a editor, check if he has a role - # Get user roles + ######### + # Roles verification + ######### user_roles = user.roles - # TODO: Check if the org_id of the role is the same as the org_id of the element using find - if action != "create": - await check_user_role_org_with_element_org(request, element_id, user_roles) - - # Check if user has the right role - - element_type = await check_element_type(element_id) - for role_id in user_roles: - role = RoleInDB(**await roles.find_one({"role_id": role_id})) - if role.elements[element_type][f"action_{action}"]: - return True + return await check_user_role_org_with_element_org(request, element_id, user_roles, action) # If no role is found, raise an error raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail="You don't have the right to perform this action") + status_code=status.HTTP_403_FORBIDDEN, + detail="User rights : You don't have the right to perform this action", + ) async def check_element_type(element_id): @@ -104,11 +104,17 @@ async def check_element_type(element_id): return "activities" else: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Issue verifying element nature") + status_code=status.HTTP_409_CONFLICT, + detail="User rights : Issue verifying element nature", + ) -async def check_user_role_org_with_element_org(request: Request, element_id: str, roles_list: list[UserRolesInOrganization]): - +async def check_user_role_org_with_element_org( + request: Request, + element_id: str, + roles_list: list[UserRolesInOrganization], + action: str, +): element_type = await check_element_type(element_id) element = request.app.db[element_type] roles = request.app.db["roles"] @@ -117,19 +123,27 @@ async def check_user_role_org_with_element_org(request: Request, element_id: str singular_form_element = element_type[:-1] element_type_id = singular_form_element + "_id" - + element_org = await element.find_one({element_type_id: element_id}) - for role_id in roles_list: - role = RoleInDB(**await roles.find_one({"role_id": role_id})) - if role.org_id == element_org["org_id"]: - return True - if role.org_id == "*": - return True + for role in roles_list: + # 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 == "*": + # Check if user has the right role + for role in roles_list: + role_db = await roles.find_one({"role_id": role.role_id}) + role = RoleInDB(**role_db) + if role.elements[element_type][f"action_{action}"]: + return True else: raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail="You don't have the right to perform this action") + status_code=status.HTTP_403_FORBIDDEN, + detail="User rights (roles) : You don't have the right to perform this action", + ) + ### 🔒 Roles checking ############################################################## diff --git a/src/services/courses/activities/activities.py b/src/services/courses/activities/activities.py index 8816461a..e04bf2f9 100644 --- a/src/services/courses/activities/activities.py +++ b/src/services/courses/activities/activities.py @@ -84,6 +84,11 @@ async def get_activity(request: Request, activity_id: str, current_user: PublicU course = await courses.find_one({"chapters": coursechapter_id}) isCoursePublic = course["public"] + isAuthor = current_user.user_id in course["authors"] + + if isAuthor: + activity = ActivityInDB(**activity) + return activity # verify course rights hasRoleRights = await verify_user_rights_with_roles( diff --git a/src/services/courses/courses.py b/src/services/courses/courses.py index bd6f2786..a7c65022 100644 --- a/src/services/courses/courses.py +++ b/src/services/courses/courses.py @@ -374,6 +374,11 @@ async def verify_rights( course = await courses.find_one({"course_id": course_id}) + isAuthor = current_user.user_id in course["authors"] + + if isAuthor: + return True + if ( current_user.user_id == "anonymous" and course["public"] is True @@ -390,7 +395,7 @@ async def verify_rights( hasRoleRights = await verify_user_rights_with_roles( request, action, current_user.user_id, course_id, course["org_id"] ) - isAuthor = current_user.user_id in course["authors"] + if not hasRoleRights and not isAuthor: raise HTTPException(