diff --git a/front/app/organizations/new/page.tsx b/front/app/organizations/new/page.tsx index 399623c5..d80467e0 100644 --- a/front/app/organizations/new/page.tsx +++ b/front/app/organizations/new/page.tsx @@ -28,7 +28,8 @@ const Organizations = () => { const handleSubmit = async (e: any) => { e.preventDefault(); console.log({ name, description, email }); - const status = await createNewOrganization({ name, description, email, slug , default: false }); + let logo = '' + const status = await createNewOrganization({ name, description, email, logo, slug, default: false }); alert(JSON.stringify(status)); }; diff --git a/front/app/orgs/[orgslug]/(withmenu)/courses/courses.tsx b/front/app/orgs/[orgslug]/(withmenu)/courses/courses.tsx index 018f731a..19f9e17e 100644 --- a/front/app/orgs/[orgslug]/(withmenu)/courses/courses.tsx +++ b/front/app/orgs/[orgslug]/(withmenu)/courses/courses.tsx @@ -30,6 +30,8 @@ function Courses(props: CourseProps) { async function deleteCourses(course_id: any) { await deleteCourseFromBackend(course_id); revalidateTags(['courses']); + // terrible, nextjs right now doesn't mutate the page when the data changes + window.location.reload(); } async function closeNewCourseModal() { diff --git a/front/app/orgs/[orgslug]/(withmenu)/layout.tsx b/front/app/orgs/[orgslug]/(withmenu)/layout.tsx index a74dcc9c..05df761a 100644 --- a/front/app/orgs/[orgslug]/(withmenu)/layout.tsx +++ b/front/app/orgs/[orgslug]/(withmenu)/layout.tsx @@ -1,8 +1,8 @@ import "@styles/globals.css"; -import { Menu } from "@components/UI/Elements/Menu"; +import { Menu } from "@components/UI/Elements/Menu/Menu"; import AuthProvider from "@components/Security/AuthProvider"; -export default function RootLayout({ children, params }: { children: React.ReactNode , params:any}) { +export default async function RootLayout({ children, params }: { children: React.ReactNode , params:any}) { return ( <> diff --git a/front/app/orgs/[orgslug]/settings/organization/general/organization.tsx b/front/app/orgs/[orgslug]/settings/organization/general/organization.tsx index df5cdb9a..bc26be95 100644 --- a/front/app/orgs/[orgslug]/settings/organization/general/organization.tsx +++ b/front/app/orgs/[orgslug]/settings/organization/general/organization.tsx @@ -1,23 +1,51 @@ "use client"; -import React from 'react' +import React, { useState } from 'react' import { Field, Form, Formik } from 'formik'; -import { updateOrganization } from '@services/settings/org'; +import { updateOrganization, uploadOrganizationLogo } from '@services/settings/org'; +import { UploadCloud } from 'lucide-react'; +import { revalidateTags } from '@services/utils/ts/requests'; interface OrganizationValues { name: string; description: string; slug: string; + logo: string; email: string; } function OrganizationClient(props: any) { + const [selectedFile, setSelectedFile] = useState(null); + + // ... + + const handleFileChange = (event: React.ChangeEvent) => { + if (event.target.files && event.target.files.length > 0) { + const file = event.target.files[0]; + setSelectedFile(file); + } + }; + + const uploadLogo = async () => { + if (selectedFile) { + let org_id = org.org_id; + await uploadOrganizationLogo(org_id, selectedFile); + setSelectedFile(null); // Reset the selected file + revalidateTags(['organizations']); + // reload the page + // terrible hack, it will fixed later + window.location.reload(); + } + }; + + const org = props.org; let orgValues: OrganizationValues = { name: org.name, description: org.description, slug: org.slug, + logo: org.logo, email: org.email } @@ -26,6 +54,7 @@ function OrganizationClient(props: any) { await updateOrganization(org_id, values); } + return (

Organization Settings

@@ -62,6 +91,28 @@ function OrganizationClient(props: any) { name="description" /> + + +
+ + +
+ + diff --git a/front/components/Modals/Course/Create/CreateCourse.tsx b/front/components/Modals/Course/Create/CreateCourse.tsx index 96a7456e..f0d0aa9a 100644 --- a/front/components/Modals/Course/Create/CreateCourse.tsx +++ b/front/components/Modals/Course/Create/CreateCourse.tsx @@ -46,6 +46,9 @@ function CreateCourseModal({ closeModal, orgslug }: any) { if (status.org_id == orgId) { closeModal(); + // reload the page + // terrible, nextjs right now doesn't mutate the page when the data changes + window.location.reload(); } else { alert("Error creating course, please see console logs"); console.log(status); diff --git a/front/components/Security/HeaderProfileBox.tsx b/front/components/Security/HeaderProfileBox.tsx index b5fbd628..43015d9e 100644 --- a/front/components/Security/HeaderProfileBox.tsx +++ b/front/components/Security/HeaderProfileBox.tsx @@ -1,3 +1,4 @@ +'use client'; import React from "react"; import styled from "styled-components"; import Link from "next/link"; diff --git a/front/components/UI/Elements/Menu.tsx b/front/components/UI/Elements/Menu.tsx deleted file mode 100644 index e7222930..00000000 --- a/front/components/UI/Elements/Menu.tsx +++ /dev/null @@ -1,129 +0,0 @@ -"use client"; -import React from "react"; -import styled from "styled-components"; -import { HeaderProfileBox } from "../../Security/HeaderProfileBox"; -import learnhouseIcon from "public/learnhouse_icon.png"; -import learnhouseLogo from "public/learnhouse_logo.png"; -import Link from "next/link"; -import Image from "next/image"; -import { getUriWithOrg } from "@services/config/config"; -import ToolTip from "../Tooltip/Tooltip"; - -export const Menu = (props : any ) => { - const orgslug = props.orgslug; - - - return ( - <> -
- - - - - - - - -
-
- - - -

{process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA}

-
}>pre-alpha - - - -
    -
  • - Courses -
  • -
  • - Collections -
  • -
  • - {" "} - Trail -
  • -
-
- - - ); -}; - -const GlobalHeader = styled.div` - display: flex; - height: 60px; - background: #ffffff; - box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.03); -`; - -const LogoArea = styled.div` - display: flex; - place-items: stretch; -`; - -const PreAlphaLabel = styled.div` - display: flex; - place-items: center; - border-radius: 6px; - height: 50%; - border: none; - margin-top: 20px; - margin-bottom: 20px; - padding-left: 10px; - padding-right: 10px; - font-size: 12px; - font-weight: bolder; - text-transform: uppercase; -`; - - -const Logo = styled.div` - display: flex; - place-items: center; - padding-left: 20px; - a { - margin: 0; - padding-left: 10px; - padding-top: 2px; - } -`; - -const SearchArea = styled.div` - display: flex; - place-items: stretch; - flex-grow: 2; -`; - -const Search = styled.div` - display: flex; - place-items: center; - padding-left: 20px; - width: auto; -`; - - - -const MenuArea = styled.div` - display: flex; - place-items: stretch; - flex-grow: 1; - - ul { - display: flex; - place-items: center; - list-style: none; - padding-left: 20px; - - li { - padding-right: 20px; - font-size: 16px; - font-weight: 500; - color: #525252; - } - } -`; diff --git a/front/components/UI/Elements/Menu/Menu.tsx b/front/components/UI/Elements/Menu/Menu.tsx new file mode 100644 index 00000000..804e0952 --- /dev/null +++ b/front/components/UI/Elements/Menu/Menu.tsx @@ -0,0 +1,105 @@ + +import React from "react"; +import learnhouseLogo from "public/learnhouse_logo.png"; +import Link from "next/link"; +import Image from "next/image"; +import { getBackendUrl, getUriWithOrg } from "@services/config/config"; +import { getOrganizationContextInfo, getOrganizationContextInfoNoAsync } from "@services/organizations/orgs"; +import ClientComponentSkeleton from "@components/UI/Utils/ClientComp"; +import { HeaderProfileBox } from "@components/Security/HeaderProfileBox"; + +export const Menu = async (props: any) => { + const orgslug = props.orgslug; + const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] }); + console.log(org); + + + return ( + <> +
+
+
+ +
+ {org?.logo ? ( + Learnhouse + ) : ( + + )} +
+ +
+
+
    + + + +
+
+
+ + + +
+
+ + ); +}; + +const LinkItem = (props: any, orgslug: any) => { + const link = props.link; + return ( + +
  • + {props.type == 'courses' && + <> + + + + Courses + } + + {props.type == 'collections' && + <> + + + + Collections + } + + {props.type == 'trail' && + <> + + + + Trail + } +
  • + + ) +} + + +const LearnHouseLogo = () => { + return ( + + + + + + + + + + + + + + ) + +} \ No newline at end of file diff --git a/front/components/UI/Elements/Menu/ProfileArea.tsx b/front/components/UI/Elements/Menu/ProfileArea.tsx new file mode 100644 index 00000000..e912f092 --- /dev/null +++ b/front/components/UI/Elements/Menu/ProfileArea.tsx @@ -0,0 +1,156 @@ +"use client"; +import React from "react"; +import styled from "styled-components"; +import Link from "next/link"; +import Avvvatars from "avvvatars-react"; +import { GearIcon } from "@radix-ui/react-icons"; +import { getRefreshToken, getUserInfo } from "@services/auth/auth"; +import { usePathname } from "next/navigation"; +import { useRouter } from "next/router"; + +export interface Auth { + access_token: string; + isAuthenticated: boolean; + userInfo: any; + isLoading: boolean; +} + +function ProfileArea() { + + + const PRIVATE_ROUTES = ["/course/*/edit", "/settings*", "/trail"]; + const NON_AUTHENTICATED_ROUTES = ["/login", "/register"]; + + + const router = useRouter(); + const pathname = usePathname(); + const [auth, setAuth] = React.useState({ access_token: "", isAuthenticated: false, userInfo: {}, isLoading: true }); + + async function checkRefreshToken() { + let data = await getRefreshToken(); + if (data) { + return data.access_token; + } + } + + + async function checkAuth() { + try { + let access_token = await checkRefreshToken(); + let userInfo = {}; + let isLoading = false; + + if (access_token) { + userInfo = await getUserInfo(access_token); + setAuth({ access_token, isAuthenticated: true, userInfo, isLoading }); + + // Redirect to home if user is trying to access a NON_AUTHENTICATED_ROUTES route + + if (NON_AUTHENTICATED_ROUTES.some((route) => new RegExp(`^${route.replace("*", ".*")}$`).test(pathname))) { + router.push("/"); + } + + + } else { + setAuth({ access_token, isAuthenticated: false, userInfo, isLoading }); + + // Redirect to login if user is trying to access a private route + if (PRIVATE_ROUTES.some((route) => new RegExp(`^${route.replace("*", ".*")}$`).test(pathname))) { + router.push("/login"); + } + + } + } catch (error) { + + } + } + return ( + + {!auth.isAuthenticated && ( + +
      +
    • + + Login + +
    • +
    • + + Sign up + +
    • +
    +
    + )} + {auth.isAuthenticated && ( + +
    {auth.userInfo.user_object.username}
    +
    + +
    + +
    + )} +
    + ) +} + +const AccountArea = styled.div` + padding-right: 20px; + display: flex; + place-items: center; + + a{ + // center the gear icon + display: flex; + place-items: center; + place-content: center; + width: 29px; + height: 29px; + border-radius: 19px; + background: #F5F5F5; + + // hover effect + &:hover{ + background: #E5E5E5; + + } + } + + div { + margin-right: 10px; + } + img { + width: 29px; + border-radius: 19px; + } +`; + +const ProfileAreaStyled = styled.div` + display: flex; + place-items: stretch; + place-items: center; +`; + +const UnidentifiedArea = styled.div` + display: flex; + place-items: stretch; + flex-grow: 1; + + ul { + display: flex; + place-items: center; + list-style: none; + padding-left: 20px; + + li { + padding-right: 20px; + font-size: 16px; + font-weight: 500; + color: #171717; + } + } +`; + + +export default ProfileArea \ No newline at end of file diff --git a/front/components/UI/Utils/ClientComp.tsx b/front/components/UI/Utils/ClientComp.tsx new file mode 100644 index 00000000..1b1f74ab --- /dev/null +++ b/front/components/UI/Utils/ClientComp.tsx @@ -0,0 +1,13 @@ +"use client"; + +function ClientComponentSkeleton({ + children, +}: { + children: React.ReactNode +}) { + return ( +
    {children}
    + ) +} + +export default ClientComponentSkeleton \ No newline at end of file diff --git a/front/middleware.ts b/front/middleware.ts index 983566d7..4a8f5280 100644 --- a/front/middleware.ts +++ b/front/middleware.ts @@ -25,7 +25,7 @@ export default function middleware(req: NextRequest) { // Organizations & Global settings if (pathname.startsWith("/organizations")) { - return NextResponse.rewrite(new URL("/organizations", req.url)); + return NextResponse.rewrite(new URL(pathname, req.url)); } // Dynamic Pages Editor diff --git a/front/package-lock.json b/front/package-lock.json index f71c3d35..6e8c8ac1 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.104.1", - "next": "^13.4.3", + "next": "^13.4.6", "re-resizable": "^6.9.9", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", @@ -2123,9 +2123,9 @@ } }, "node_modules/@next/env": { - "version": "13.4.3", - "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.3.tgz", - "integrity": "sha512-pa1ErjyFensznttAk3EIv77vFbfSYT6cLzVRK5jx4uiRuCQo+m2wCFAREaHKIy63dlgvOyMlzh6R8Inu8H3KrQ==" + "version": "13.4.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.6.tgz", + "integrity": "sha512-nqUxEtvDqFhmV1/awSg0K2XHNwkftNaiUqCYO9e6+MYmqNObpKVl7OgMkGaQ2SZnFx5YqF0t60ZJTlyJIDAijg==" }, "node_modules/@next/eslint-plugin-next": { "version": "13.0.6", @@ -2137,9 +2137,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "13.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.3.tgz", - "integrity": "sha512-yx18udH/ZmR4Bw4M6lIIPE3JxsAZwo04iaucEfA2GMt1unXr2iodHUX/LAKNyi6xoLP2ghi0E+Xi1f4Qb8f1LQ==", + "version": "13.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.6.tgz", + "integrity": "sha512-ahi6VP98o4HV19rkOXPSUu+ovfHfUxbJQ7VVJ7gL2FnZRr7onEFC1oGQ6NQHpm8CxpIzSSBW79kumlFMOmZVjg==", "cpu": [ "arm64" ], @@ -2152,9 +2152,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "13.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.3.tgz", - "integrity": "sha512-Mi8xJWh2IOjryAM1mx18vwmal9eokJ2njY4nDh04scy37F0LEGJ/diL6JL6kTXi0UfUCGbMsOItf7vpReNiD2A==", + "version": "13.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.6.tgz", + "integrity": "sha512-13cXxKFsPJIJKzUqrU5XB1mc0xbUgYsRcdH6/rB8c4NMEbWGdtD4QoK9ShN31TZdePpD4k416Ur7p+deMIxnnA==", "cpu": [ "x64" ], @@ -2167,9 +2167,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "13.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.3.tgz", - "integrity": "sha512-aBvtry4bxJ1xwKZ/LVPeBGBwWVwxa4bTnNkRRw6YffJnn/f4Tv4EGDPaVeYHZGQVA56wsGbtA6nZMuWs/EIk4Q==", + "version": "13.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.6.tgz", + "integrity": "sha512-Ti+NMHEjTNktCVxNjeWbYgmZvA2AqMMI2AMlzkXsU7W4pXCMhrryAmAIoo+7YdJbsx01JQWYVxGe62G6DoCLaA==", "cpu": [ "arm64" ], @@ -2182,9 +2182,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "13.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.3.tgz", - "integrity": "sha512-krT+2G3kEsEUvZoYte3/2IscscDraYPc2B+fDJFipPktJmrv088Pei/RjrhWm5TMIy5URYjZUoDZdh5k940Dyw==", + "version": "13.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.6.tgz", + "integrity": "sha512-OHoC6gO7XfjstgwR+z6UHKlvhqJfyMtNaJidjx3sEcfaDwS7R2lqR5AABi8PuilGgi0BO0O0sCXqLlpp3a0emQ==", "cpu": [ "arm64" ], @@ -2197,9 +2197,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "13.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.3.tgz", - "integrity": "sha512-AMdFX6EKJjC0G/CM6hJvkY8wUjCcbdj3Qg7uAQJ7PVejRWaVt0sDTMavbRfgMchx8h8KsAudUCtdFkG9hlEClw==", + "version": "13.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.6.tgz", + "integrity": "sha512-zHZxPGkUlpfNJCboUrFqwlwEX5vI9LSN70b8XEb0DYzzlrZyCyOi7hwDp/+3Urm9AB7YCAJkgR5Sp1XBVjHdfQ==", "cpu": [ "x64" ], @@ -2212,9 +2212,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "13.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.3.tgz", - "integrity": "sha512-jySgSXE48shaLtcQbiFO9ajE9mqz7pcAVLnVLvRIlUHyQYR/WyZdK8ehLs65Mz6j9cLrJM+YdmdJPyV4WDaz2g==", + "version": "13.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.6.tgz", + "integrity": "sha512-K/Y8lYGTwTpv5ME8PSJxwxLolaDRdVy+lOd9yMRMiQE0BLUhtxtCWC9ypV42uh9WpLjoaD0joOsB9Q6mbrSGJg==", "cpu": [ "x64" ], @@ -2227,9 +2227,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "13.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.3.tgz", - "integrity": "sha512-5DxHo8uYcaADiE9pHrg8o28VMt/1kR8voDehmfs9AqS0qSClxAAl+CchjdboUvbCjdNWL1MISCvEfKY2InJ3JA==", + "version": "13.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.6.tgz", + "integrity": "sha512-U6LtxEUrjBL2tpW+Kr1nHCSJWNeIed7U7l5o7FiKGGwGgIlFi4UHDiLI6TQ2lxi20fAU33CsruV3U0GuzMlXIw==", "cpu": [ "arm64" ], @@ -2242,9 +2242,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "13.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.3.tgz", - "integrity": "sha512-LaqkF3d+GXRA5X6zrUjQUrXm2MN/3E2arXBtn5C7avBCNYfm9G3Xc646AmmmpN3DJZVaMYliMyCIQCMDEzk80w==", + "version": "13.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.6.tgz", + "integrity": "sha512-eEBeAqpCfhdPSlCZCayjCiyIllVqy4tcqvm1xmg3BgJG0G5ITiMM4Cw2WVeRSgWDJqQGRyyb+q8Y2ltzhXOWsQ==", "cpu": [ "ia32" ], @@ -2257,9 +2257,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "13.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.3.tgz", - "integrity": "sha512-jglUk/x7ZWeOJWlVoKyIAkHLTI+qEkOriOOV+3hr1GyiywzcqfI7TpFSiwC7kk1scOiH7NTFKp8mA3XPNO9bDw==", + "version": "13.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.6.tgz", + "integrity": "sha512-OrZs94AuO3ZS5tnqlyPRNgfWvboXaDQCi5aXGve3o3C+Sj0ctMUV9+Do+0zMvvLRumR8E0PTWKvtz9n5vzIsWw==", "cpu": [ "x64" ], @@ -5469,6 +5469,11 @@ "node": ">=10.13.0" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, "node_modules/globals": { "version": "13.17.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", @@ -5539,8 +5544,7 @@ "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, "node_modules/grapheme-splitter": { "version": "1.0.4", @@ -6371,16 +6375,17 @@ "dev": true }, "node_modules/next": { - "version": "13.4.3", - "resolved": "https://registry.npmjs.org/next/-/next-13.4.3.tgz", - "integrity": "sha512-FV3pBrAAnAIfOclTvncw9dDohyeuEEXPe5KNcva91anT/rdycWbgtu3IjUj4n5yHnWK8YEPo0vrUecHmnmUNbA==", + "version": "13.4.6", + "resolved": "https://registry.npmjs.org/next/-/next-13.4.6.tgz", + "integrity": "sha512-sjVqjxU+U2aXZnYt4Ud6CTLNNwWjdSfMgemGpIQJcN3Z7Jni9xRWbR0ie5fQzCg87aLqQVhKA2ud2gPoqJ9lGw==", "dependencies": { - "@next/env": "13.4.3", + "@next/env": "13.4.6", "@swc/helpers": "0.5.1", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001406", "postcss": "8.4.14", "styled-jsx": "5.1.1", + "watchpack": "2.4.0", "zod": "3.21.4" }, "bin": { @@ -6390,20 +6395,19 @@ "node": ">=16.8.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "13.4.3", - "@next/swc-darwin-x64": "13.4.3", - "@next/swc-linux-arm64-gnu": "13.4.3", - "@next/swc-linux-arm64-musl": "13.4.3", - "@next/swc-linux-x64-gnu": "13.4.3", - "@next/swc-linux-x64-musl": "13.4.3", - "@next/swc-win32-arm64-msvc": "13.4.3", - "@next/swc-win32-ia32-msvc": "13.4.3", - "@next/swc-win32-x64-msvc": "13.4.3" + "@next/swc-darwin-arm64": "13.4.6", + "@next/swc-darwin-x64": "13.4.6", + "@next/swc-linux-arm64-gnu": "13.4.6", + "@next/swc-linux-arm64-musl": "13.4.6", + "@next/swc-linux-x64-gnu": "13.4.6", + "@next/swc-linux-x64-musl": "13.4.6", + "@next/swc-win32-arm64-msvc": "13.4.6", + "@next/swc-win32-ia32-msvc": "13.4.6", + "@next/swc-win32-x64-msvc": "13.4.6" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "fibers": ">= 3.1.0", - "node-sass": "^6.0.0 || ^7.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" @@ -6415,9 +6419,6 @@ "fibers": { "optional": true }, - "node-sass": { - "optional": true - }, "sass": { "optional": true } @@ -8369,6 +8370,18 @@ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz", "integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==" }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -10043,9 +10056,9 @@ } }, "@next/env": { - "version": "13.4.3", - "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.3.tgz", - "integrity": "sha512-pa1ErjyFensznttAk3EIv77vFbfSYT6cLzVRK5jx4uiRuCQo+m2wCFAREaHKIy63dlgvOyMlzh6R8Inu8H3KrQ==" + "version": "13.4.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.6.tgz", + "integrity": "sha512-nqUxEtvDqFhmV1/awSg0K2XHNwkftNaiUqCYO9e6+MYmqNObpKVl7OgMkGaQ2SZnFx5YqF0t60ZJTlyJIDAijg==" }, "@next/eslint-plugin-next": { "version": "13.0.6", @@ -10057,57 +10070,57 @@ } }, "@next/swc-darwin-arm64": { - "version": "13.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.3.tgz", - "integrity": "sha512-yx18udH/ZmR4Bw4M6lIIPE3JxsAZwo04iaucEfA2GMt1unXr2iodHUX/LAKNyi6xoLP2ghi0E+Xi1f4Qb8f1LQ==", + "version": "13.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.6.tgz", + "integrity": "sha512-ahi6VP98o4HV19rkOXPSUu+ovfHfUxbJQ7VVJ7gL2FnZRr7onEFC1oGQ6NQHpm8CxpIzSSBW79kumlFMOmZVjg==", "optional": true }, "@next/swc-darwin-x64": { - "version": "13.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.3.tgz", - "integrity": "sha512-Mi8xJWh2IOjryAM1mx18vwmal9eokJ2njY4nDh04scy37F0LEGJ/diL6JL6kTXi0UfUCGbMsOItf7vpReNiD2A==", + "version": "13.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.6.tgz", + "integrity": "sha512-13cXxKFsPJIJKzUqrU5XB1mc0xbUgYsRcdH6/rB8c4NMEbWGdtD4QoK9ShN31TZdePpD4k416Ur7p+deMIxnnA==", "optional": true }, "@next/swc-linux-arm64-gnu": { - "version": "13.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.3.tgz", - "integrity": "sha512-aBvtry4bxJ1xwKZ/LVPeBGBwWVwxa4bTnNkRRw6YffJnn/f4Tv4EGDPaVeYHZGQVA56wsGbtA6nZMuWs/EIk4Q==", + "version": "13.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.6.tgz", + "integrity": "sha512-Ti+NMHEjTNktCVxNjeWbYgmZvA2AqMMI2AMlzkXsU7W4pXCMhrryAmAIoo+7YdJbsx01JQWYVxGe62G6DoCLaA==", "optional": true }, "@next/swc-linux-arm64-musl": { - "version": "13.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.3.tgz", - "integrity": "sha512-krT+2G3kEsEUvZoYte3/2IscscDraYPc2B+fDJFipPktJmrv088Pei/RjrhWm5TMIy5URYjZUoDZdh5k940Dyw==", + "version": "13.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.6.tgz", + "integrity": "sha512-OHoC6gO7XfjstgwR+z6UHKlvhqJfyMtNaJidjx3sEcfaDwS7R2lqR5AABi8PuilGgi0BO0O0sCXqLlpp3a0emQ==", "optional": true }, "@next/swc-linux-x64-gnu": { - "version": "13.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.3.tgz", - "integrity": "sha512-AMdFX6EKJjC0G/CM6hJvkY8wUjCcbdj3Qg7uAQJ7PVejRWaVt0sDTMavbRfgMchx8h8KsAudUCtdFkG9hlEClw==", + "version": "13.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.6.tgz", + "integrity": "sha512-zHZxPGkUlpfNJCboUrFqwlwEX5vI9LSN70b8XEb0DYzzlrZyCyOi7hwDp/+3Urm9AB7YCAJkgR5Sp1XBVjHdfQ==", "optional": true }, "@next/swc-linux-x64-musl": { - "version": "13.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.3.tgz", - "integrity": "sha512-jySgSXE48shaLtcQbiFO9ajE9mqz7pcAVLnVLvRIlUHyQYR/WyZdK8ehLs65Mz6j9cLrJM+YdmdJPyV4WDaz2g==", + "version": "13.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.6.tgz", + "integrity": "sha512-K/Y8lYGTwTpv5ME8PSJxwxLolaDRdVy+lOd9yMRMiQE0BLUhtxtCWC9ypV42uh9WpLjoaD0joOsB9Q6mbrSGJg==", "optional": true }, "@next/swc-win32-arm64-msvc": { - "version": "13.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.3.tgz", - "integrity": "sha512-5DxHo8uYcaADiE9pHrg8o28VMt/1kR8voDehmfs9AqS0qSClxAAl+CchjdboUvbCjdNWL1MISCvEfKY2InJ3JA==", + "version": "13.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.6.tgz", + "integrity": "sha512-U6LtxEUrjBL2tpW+Kr1nHCSJWNeIed7U7l5o7FiKGGwGgIlFi4UHDiLI6TQ2lxi20fAU33CsruV3U0GuzMlXIw==", "optional": true }, "@next/swc-win32-ia32-msvc": { - "version": "13.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.3.tgz", - "integrity": "sha512-LaqkF3d+GXRA5X6zrUjQUrXm2MN/3E2arXBtn5C7avBCNYfm9G3Xc646AmmmpN3DJZVaMYliMyCIQCMDEzk80w==", + "version": "13.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.6.tgz", + "integrity": "sha512-eEBeAqpCfhdPSlCZCayjCiyIllVqy4tcqvm1xmg3BgJG0G5ITiMM4Cw2WVeRSgWDJqQGRyyb+q8Y2ltzhXOWsQ==", "optional": true }, "@next/swc-win32-x64-msvc": { - "version": "13.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.3.tgz", - "integrity": "sha512-jglUk/x7ZWeOJWlVoKyIAkHLTI+qEkOriOOV+3hr1GyiywzcqfI7TpFSiwC7kk1scOiH7NTFKp8mA3XPNO9bDw==", + "version": "13.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.6.tgz", + "integrity": "sha512-OrZs94AuO3ZS5tnqlyPRNgfWvboXaDQCi5aXGve3o3C+Sj0ctMUV9+Do+0zMvvLRumR8E0PTWKvtz9n5vzIsWw==", "optional": true }, "@nicolo-ribaudo/chokidar-2": { @@ -12423,6 +12436,11 @@ "is-glob": "^4.0.3" } }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, "globals": { "version": "13.17.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", @@ -12476,8 +12494,7 @@ "graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, "grapheme-splitter": { "version": "1.0.4", @@ -13076,25 +13093,26 @@ "dev": true }, "next": { - "version": "13.4.3", - "resolved": "https://registry.npmjs.org/next/-/next-13.4.3.tgz", - "integrity": "sha512-FV3pBrAAnAIfOclTvncw9dDohyeuEEXPe5KNcva91anT/rdycWbgtu3IjUj4n5yHnWK8YEPo0vrUecHmnmUNbA==", + "version": "13.4.6", + "resolved": "https://registry.npmjs.org/next/-/next-13.4.6.tgz", + "integrity": "sha512-sjVqjxU+U2aXZnYt4Ud6CTLNNwWjdSfMgemGpIQJcN3Z7Jni9xRWbR0ie5fQzCg87aLqQVhKA2ud2gPoqJ9lGw==", "requires": { - "@next/env": "13.4.3", - "@next/swc-darwin-arm64": "13.4.3", - "@next/swc-darwin-x64": "13.4.3", - "@next/swc-linux-arm64-gnu": "13.4.3", - "@next/swc-linux-arm64-musl": "13.4.3", - "@next/swc-linux-x64-gnu": "13.4.3", - "@next/swc-linux-x64-musl": "13.4.3", - "@next/swc-win32-arm64-msvc": "13.4.3", - "@next/swc-win32-ia32-msvc": "13.4.3", - "@next/swc-win32-x64-msvc": "13.4.3", + "@next/env": "13.4.6", + "@next/swc-darwin-arm64": "13.4.6", + "@next/swc-darwin-x64": "13.4.6", + "@next/swc-linux-arm64-gnu": "13.4.6", + "@next/swc-linux-arm64-musl": "13.4.6", + "@next/swc-linux-x64-gnu": "13.4.6", + "@next/swc-linux-x64-musl": "13.4.6", + "@next/swc-win32-arm64-msvc": "13.4.6", + "@next/swc-win32-ia32-msvc": "13.4.6", + "@next/swc-win32-x64-msvc": "13.4.6", "@swc/helpers": "0.5.1", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001406", "postcss": "8.4.14", "styled-jsx": "5.1.1", + "watchpack": "2.4.0", "zod": "3.21.4" }, "dependencies": { @@ -14432,6 +14450,15 @@ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz", "integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==" }, + "watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/front/package.json b/front/package.json index 55ed4d6b..68ad246c 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.104.1", - "next": "^13.4.3", + "next": "^13.4.6", "re-resizable": "^6.9.9", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", diff --git a/front/services/organizations/orgs.ts b/front/services/organizations/orgs.ts index 56a11a94..1a2c6d32 100644 --- a/front/services/organizations/orgs.ts +++ b/front/services/organizations/orgs.ts @@ -19,7 +19,12 @@ export async function deleteOrganizationFromBackend(org_id: any) { } export async function getOrganizationContextInfo(org_slug: any, next: any) { - const result = await fetch(`${getAPIUrl()}orgs/slug/${org_slug}`, RequestBody("GET", null,next)); + const result = await fetch(`${getAPIUrl()}orgs/slug/${org_slug}`, RequestBody("GET", null, next)); const res = await errorHandling(result); return res; } + +export function getOrganizationContextInfoNoAsync(org_slug: any, next: any) { + const result = fetch(`${getAPIUrl()}orgs/slug/${org_slug}`, RequestBody("GET", null, next)); + return result; +} \ No newline at end of file diff --git a/front/services/settings/org.ts b/front/services/settings/org.ts index 786eb56a..3007f8a7 100644 --- a/front/services/settings/org.ts +++ b/front/services/settings/org.ts @@ -1,5 +1,5 @@ import { getAPIUrl } from "@services/config/config"; -import { RequestBody, errorHandling } from "@services/utils/ts/requests"; +import { RequestBody, errorHandling, RequestBodyForm } from "@services/utils/ts/requests"; /* This file includes only POST, PUT, DELETE requests @@ -11,3 +11,12 @@ export async function updateOrganization(org_id: string, data: any) { const res = await errorHandling(result); return res; } + +export async function uploadOrganizationLogo(org_id: string, logo_file: any) { + // Send file thumbnail as form data + const formData = new FormData(); + formData.append("logo_file", logo_file); + const result: any = await fetch(`${getAPIUrl()}orgs/` + org_id + "/logo", RequestBodyForm("PUT", formData, null)); + const res = await errorHandling(result); + return res; +} \ No newline at end of file diff --git a/src/routers/orgs.py b/src/routers/orgs.py index 579c0c02..f3e2928f 100644 --- a/src/routers/orgs.py +++ b/src/routers/orgs.py @@ -1,7 +1,7 @@ -from fastapi import APIRouter, Depends, Request +from fastapi import APIRouter, Depends, Request, UploadFile from src.security.auth import get_current_user -from src.services.orgs import Organization, create_org, delete_org, get_organization, get_organization_by_slug, get_orgs_by_user, update_org +from src.services.orgs.orgs import Organization, create_org, delete_org, get_organization, get_organization_by_slug, get_orgs_by_user, update_org, update_org_logo from src.services.users.users import PublicUser, User @@ -31,6 +31,12 @@ async def api_get_org_by_slug(request: Request, org_slug: str, current_user: Use """ return await get_organization_by_slug(request, org_slug) +@router.put("/{org_id}/logo") +async def api_update_org_logo(request: Request, org_id: str, logo_file:UploadFile, current_user: PublicUser = Depends(get_current_user)): + """ + Get single Org by Slug + """ + return await update_org_logo(request=request,logo_file=logo_file, org_id=org_id, current_user=current_user) @router.get("/user/page/{page}/limit/{limit}") async def api_user_orgs(request: Request, page: int, limit: int, current_user: PublicUser = Depends(get_current_user)): diff --git a/src/security/security.py b/src/security/security.py index 30c935c5..348adf41 100644 --- a/src/security/security.py +++ b/src/security/security.py @@ -3,7 +3,7 @@ from passlib.context import CryptContext from passlib.hash import pbkdf2_sha256 from src.services.roles.schemas.roles import RoleInDB -from src.services.users.schemas.users import UserInDB +from src.services.users.schemas.users import UserInDB, UserRolesInOrganization ### 🔒 JWT ############################################################## @@ -108,7 +108,7 @@ async def check_element_type(element_id): status_code=status.HTTP_409_CONFLICT, detail="Issue verifying element nature") -async def check_user_role_org_with_element_org(request: Request, element_id: str, roles_list: list[str]): +async def check_user_role_org_with_element_org(request: Request, element_id: str, roles_list: list[UserRolesInOrganization]): element_type = await check_element_type(element_id) element = request.app.db[element_type] diff --git a/src/services/blocks/utils/upload_files.py b/src/services/blocks/utils/upload_files.py index 19a79355..c9ac4e0c 100644 --- a/src/services/blocks/utils/upload_files.py +++ b/src/services/blocks/utils/upload_files.py @@ -49,6 +49,5 @@ async def upload_file_and_return_file_object(request: Request, file: UploadFile, f.write(file_binary) f.close() - # TODO: do some error handling here return uploadable_file diff --git a/src/services/courses/courses.py b/src/services/courses/courses.py index 06c20ef3..1be805f4 100644 --- a/src/services/courses/courses.py +++ b/src/services/courses/courses.py @@ -46,6 +46,7 @@ class CourseChapterInDB(CourseChapter): creationDate: str updateDate: str + #### Classes #################################################### # TODO : Add courses photo & cover upload and delete @@ -55,6 +56,7 @@ class CourseChapterInDB(CourseChapter): # CRUD #################################################### + async def get_course(request: Request, course_id: str, current_user: PublicUser): courses = request.app.db["courses"] @@ -65,7 +67,8 @@ async def get_course(request: Request, course_id: str, current_user: PublicUser) if not course: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course does not exist") + status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" + ) course = Course(**course) return course @@ -83,10 +86,12 @@ async def get_course_meta(request: Request, course_id: str, current_user: Public if not course: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course does not exist") + status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" + ) - coursechapters = await courses.find_one({"course_id": course_id}, { - "chapters_content": 1, "_id": 0}) + coursechapters = await courses.find_one( + {"course_id": course_id}, {"chapters_content": 1, "_id": 0} + ) # activities coursechapter_activityIds_global = [] @@ -103,42 +108,66 @@ async def get_course_meta(request: Request, course_id: str, current_user: Public coursechapter_activityIds_global.append(activity) chapters[coursechapter.coursechapter_id] = { - "id": coursechapter.coursechapter_id, "name": coursechapter.name, "activityIds": coursechapter_activityIds + "id": coursechapter.coursechapter_id, + "name": coursechapter.name, + "activityIds": coursechapter_activityIds, } # activities activities_list = {} - for activity in await activities.find({"activity_id": {"$in": coursechapter_activityIds_global}}).to_list(length=100): + for activity in await activities.find( + {"activity_id": {"$in": coursechapter_activityIds_global}} + ).to_list(length=100): activity = ActivityInDB(**activity) activities_list[activity.activity_id] = { - "id": activity.activity_id, "name": activity.name, "type": activity.type, "content": activity.content + "id": activity.activity_id, + "name": activity.name, + "type": activity.type, + "content": activity.content, } chapters_list_with_activities = [] for chapter in chapters: chapters_list_with_activities.append( - {"id": chapters[chapter]["id"], "name": chapters[chapter]["name"], "activities": [activities_list[activity] for activity in chapters[chapter]["activityIds"]]}) + { + "id": chapters[chapter]["id"], + "name": chapters[chapter]["name"], + "activities": [ + activities_list[activity] + for activity in chapters[chapter]["activityIds"] + ], + } + ) course = CourseInDB(**course) # Get activity by user trail = await trails.find_one( - {"courses.course_id": course_id, "user_id": current_user.user_id}) + {"courses.course_id": course_id, "user_id": current_user.user_id} + ) print(trail) if trail: # get only the course where course_id == course_id trail_course = next( - (course for course in trail["courses"] if course["course_id"] == course_id), None) + (course for course in trail["courses"] if course["course_id"] == course_id), + None, + ) else: trail_course = "" return { "course": course, "chapters": chapters_list_with_activities, - "trail": trail_course + "trail": trail_course, } -async def create_course(request: Request, course_object: Course, org_id: str, current_user: PublicUser, thumbnail_file: UploadFile | None = None): +async def create_course( + request: Request, + course_object: Course, + org_id: str, + current_user: PublicUser, + thumbnail_file: UploadFile | None = None, +): courses = request.app.db["courses"] # generate course_id with uuid4 @@ -147,27 +176,42 @@ async def create_course(request: Request, course_object: Course, org_id: str, cu # TODO(fix) : the implementation here is clearly not the best one (this entire function) course_object.org_id = org_id course_object.chapters_content = [] - await verify_user_rights_with_roles(request, "create", current_user.user_id, course_id, org_id) + await verify_user_rights_with_roles( + request, "create", current_user.user_id, course_id, org_id + ) - if thumbnail_file: - name_in_disk = f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}" + if thumbnail_file and thumbnail_file.filename: + name_in_disk = ( + f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}" + ) await upload_thumbnail(thumbnail_file, name_in_disk) course_object.thumbnail = name_in_disk - course = CourseInDB(course_id=course_id, authors=[ - current_user.user_id], creationDate=str(datetime.now()), updateDate=str(datetime.now()), **course_object.dict()) + course = CourseInDB( + course_id=course_id, + authors=[current_user.user_id], + creationDate=str(datetime.now()), + updateDate=str(datetime.now()), + **course_object.dict(), + ) course_in_db = await courses.insert_one(course.dict()) if not course_in_db: raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Unavailable database") + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Unavailable database", + ) return course.dict() -async def update_course_thumbnail(request: Request, course_id: str, current_user: PublicUser, thumbnail_file: UploadFile | None = None): - +async def update_course_thumbnail( + request: Request, + course_id: str, + current_user: PublicUser, + thumbnail_file: UploadFile | None = None, +): # verify course rights await verify_rights(request, course_id, current_user, "update") @@ -178,26 +222,34 @@ async def update_course_thumbnail(request: Request, course_id: str, current_user if course: creationDate = course["creationDate"] authors = course["authors"] - if thumbnail_file: + if thumbnail_file and thumbnail_file.filename: name_in_disk = f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}" course = Course(**course).copy(update={"thumbnail": name_in_disk}) await upload_thumbnail(thumbnail_file, name_in_disk) - updated_course = CourseInDB(course_id=course_id, creationDate=creationDate, - authors=authors, updateDate=str(datetime.now()), **course.dict()) + updated_course = CourseInDB( + course_id=course_id, + creationDate=creationDate, + authors=authors, + updateDate=str(datetime.now()), + **course.dict(), + ) - await courses.update_one({"course_id": course_id}, { - "$set": updated_course.dict()}) + await courses.update_one( + {"course_id": course_id}, {"$set": updated_course.dict()} + ) return CourseInDB(**updated_course.dict()) else: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course does not exist") + status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" + ) -async def update_course(request: Request, course_object: Course, course_id: str, current_user: PublicUser): - +async def update_course( + request: Request, course_object: Course, course_id: str, current_user: PublicUser +): # verify course rights await verify_rights(request, course_id, current_user, "update") @@ -213,20 +265,26 @@ async def update_course(request: Request, course_object: Course, course_id: str, datetime_object = datetime.now() updated_course = CourseInDB( - course_id=course_id, creationDate=creationDate, authors=authors, updateDate=str(datetime_object), **course_object.dict()) + course_id=course_id, + creationDate=creationDate, + authors=authors, + updateDate=str(datetime_object), + **course_object.dict(), + ) - await courses.update_one({"course_id": course_id}, { - "$set": updated_course.dict()}) + await courses.update_one( + {"course_id": course_id}, {"$set": updated_course.dict()} + ) return CourseInDB(**updated_course.dict()) else: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course does not exist") + status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" + ) async def delete_course(request: Request, course_id: str, current_user: PublicUser): - # verify course rights await verify_rights(request, course_id, current_user, "delete") @@ -236,7 +294,8 @@ async def delete_course(request: Request, course_id: str, current_user: PublicUs if not course: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course does not exist") + status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" + ) isDeleted = await courses.delete_one({"course_id": course_id}) @@ -244,24 +303,38 @@ async def delete_course(request: Request, course_id: str, current_user: PublicUs return {"detail": "Course deleted"} else: raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Unavailable database") + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Unavailable database", + ) + #################################################### # Misc #################################################### -async def get_courses(request: Request, page: int = 1, limit: int = 10, org_id: str | None = None): +async def get_courses( + request: Request, page: int = 1, limit: int = 10, org_id: str | None = None +): courses = request.app.db["courses"] # TODO : Get only courses that user is admin/has roles of # get all courses from database - all_courses = courses.find({"org_id": org_id}).sort( - "name", 1).skip(10 * (page - 1)).limit(limit) + all_courses = ( + courses.find({"org_id": org_id}) + .sort("name", 1) + .skip(10 * (page - 1)) + .limit(limit) + ) - return [json.loads(json.dumps(course, default=str)) for course in await all_courses.to_list(length=100)] + return [ + json.loads(json.dumps(course, default=str)) + for course in await all_courses.to_list(length=100) + ] -async def get_courses_orgslug(request: Request, page: int = 1, limit: int = 10, org_slug: str | None = None): +async def get_courses_orgslug( + request: Request, page: int = 1, limit: int = 10, org_slug: str | None = None +): courses = request.app.db["courses"] orgs = request.app.db["organizations"] # TODO : Get only courses that user is admin/has roles of @@ -271,37 +344,61 @@ async def get_courses_orgslug(request: Request, page: int = 1, limit: int = 10, if not org: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist") + status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist" + ) # get all courses from database - all_courses = courses.find({"org_id": org['org_id']}).sort( - "name", 1).skip(10 * (page - 1)).limit(limit) + all_courses = ( + courses.find({"org_id": org["org_id"]}) + .sort("name", 1) + .skip(10 * (page - 1)) + .limit(limit) + ) - return [json.loads(json.dumps(course, default=str)) for course in await all_courses.to_list(length=100)] + return [ + json.loads(json.dumps(course, default=str)) + for course in await all_courses.to_list(length=100) + ] #### Security #################################################### -async def verify_rights(request: Request, course_id: str, current_user: PublicUser | AnonymousUser, action: str): +async def verify_rights( + request: Request, + course_id: str, + current_user: PublicUser | AnonymousUser, + action: str, +): courses = request.app.db["courses"] course = await courses.find_one({"course_id": course_id}) - if current_user.user_id == "anonymous" and course["public"] is True and action == "read": + if ( + current_user.user_id == "anonymous" + and course["public"] is True + and action == "read" + ): return True if not course: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course/CourseChapter does not exist") + status_code=status.HTTP_409_CONFLICT, + detail="Course/CourseChapter does not exist", + ) - hasRoleRights = await verify_user_rights_with_roles(request, action, current_user.user_id, course_id, course["org_id"]) + 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( - status_code=status.HTTP_403_FORBIDDEN, detail="Roles/Ownership : Insufficient rights to perform this action") + status_code=status.HTTP_403_FORBIDDEN, + detail="Roles/Ownership : Insufficient rights to perform this action", + ) return True + #### Security #################################################### diff --git a/src/services/mocks/initial.py b/src/services/mocks/initial.py index 8f76684a..70bdaa7c 100644 --- a/src/services/mocks/initial.py +++ b/src/services/mocks/initial.py @@ -9,7 +9,7 @@ from src.services.courses.chapters import CourseChapter, create_coursechapter from src.services.courses.activities.activities import Activity, create_activity from src.services.users.users import PublicUser, UserInDB -from src.services.orgs import Organization, create_org +from src.services.orgs.orgs import Organization, create_org from src.services.roles.schemas.roles import Permission, Elements, RoleInDB from src.services.courses.courses import CourseInDB from faker import Faker @@ -133,6 +133,7 @@ async def create_initial_data(request: Request): description=fake.unique.text(), email=fake.unique.email(), slug=slug, + logo="", default=False ) organizations.append(org) diff --git a/src/services/orgs/__init__.py b/src/services/orgs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/services/orgs/logos.py b/src/services/orgs/logos.py new file mode 100644 index 00000000..e2e4bff8 --- /dev/null +++ b/src/services/orgs/logos.py @@ -0,0 +1,22 @@ +import os +from uuid import uuid4 + + +async def upload_org_logo(logo_file): + contents = logo_file.file.read() + name_in_disk = f"{uuid4()}.{logo_file.filename.split('.')[-1]}" + + try: + if not os.path.exists("content/uploads/logos"): + os.makedirs("content/uploads/logos") + + with open(f"content/uploads/logos/{name_in_disk}", "wb") as f: + f.write(contents) + f.close() + + except Exception: + return {"message": "There was an error uploading the file"} + finally: + logo_file.file.close() + + return name_in_disk diff --git a/src/services/orgs.py b/src/services/orgs/orgs.py similarity index 53% rename from src/services/orgs.py rename to src/services/orgs/orgs.py index ae570e75..8fc78b83 100644 --- a/src/services/orgs.py +++ b/src/services/orgs/orgs.py @@ -1,40 +1,16 @@ import json from typing import Optional from uuid import uuid4 -from click import Option -from pydantic import BaseModel +from src.services.orgs.logos import upload_org_logo +from src.services.orgs.schemas.orgs import ( + Organization, + OrganizationInDB, + PublicOrganization, +) from src.services.users.schemas.users import UserOrganization from src.services.users.users import PublicUser from src.security.security import * -from fastapi import HTTPException, status, Request - -#### Classes #################################################### - - -class Organization(BaseModel): - name: str - description: str - email: str - slug: str - default: Optional[bool] - - -class OrganizationInDB(Organization): - org_id: str - - -class PublicOrganization(Organization): - name: str - description: str - email: str - slug: str - org_id: str - - def __getitem__(self, item): - return getattr(self, item) - - -#### Classes #################################################### +from fastapi import HTTPException, UploadFile, status, Request async def get_organization(request: Request, org_id: str): @@ -44,7 +20,8 @@ async def get_organization(request: Request, org_id: str): if not org: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist") + status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist" + ) org = PublicOrganization(**org) return org @@ -57,13 +34,16 @@ async def get_organization_by_slug(request: Request, org_slug: str): if not org: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist") + status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist" + ) org = PublicOrganization(**org) return org -async def create_org(request: Request, org_object: Organization, current_user: PublicUser): +async def create_org( + request: Request, org_object: Organization, current_user: PublicUser +): orgs = request.app.db["organizations"] user = request.app.db["users"] @@ -72,7 +52,9 @@ async def create_org(request: Request, org_object: Organization, current_user: P if isOrgAvailable: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Organization slug already exists") + status_code=status.HTTP_409_CONFLICT, + detail="Organization slug already exists", + ) # generate org_id with uuid4 org_id = str(f"org_{uuid4()}") @@ -82,25 +64,33 @@ async def create_org(request: Request, org_object: Organization, current_user: P org_in_db = await orgs.insert_one(org.dict()) user_organization: UserOrganization = UserOrganization( - org_id=org_id, org_role="owner") + org_id=org_id, org_role="owner" + ) # add org to user - await user.update_one({"user_id": current_user.user_id}, { - "$addToSet": {"orgs": user_organization.dict()}}) - - # add role admin to org - await user.update_one({"user_id": current_user.user_id}, { - "$addToSet": {"roles": {"org_id": org_id, "role_id": "role_admin"}}}) + await user.update_one( + {"user_id": current_user.user_id}, + {"$addToSet": {"orgs": user_organization.dict()}}, + ) + + # add role admin to org + await user.update_one( + {"user_id": current_user.user_id}, + {"$addToSet": {"roles": {"org_id": org_id, "role_id": "role_admin"}}}, + ) if not org_in_db: raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Unavailable database") + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Unavailable database", + ) return org.dict() -async def update_org(request: Request, org_object: Organization, org_id: str, current_user: PublicUser): - +async def update_org( + request: Request, org_object: Organization, org_id: str, current_user: PublicUser +): # verify org rights await verify_org_rights(request, org_id, current_user, "update") @@ -108,21 +98,38 @@ async def update_org(request: Request, org_object: Organization, org_id: str, cu org = await orgs.find_one({"org_id": org_id}) - if not org: - - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist") - - updated_org = OrganizationInDB( - org_id=org_id, **org_object.dict()) + updated_org = OrganizationInDB(org_id=org_id, **org_object.dict()) + # update org await orgs.update_one({"org_id": org_id}, {"$set": updated_org.dict()}) - return Organization(**updated_org.dict()) + + return updated_org.dict() + + +async def update_org_logo( + request: Request, logo_file: UploadFile, org_id: str, current_user: PublicUser +): + # verify org rights + await verify_org_rights(request, org_id, current_user, "update") + + orgs = request.app.db["organizations"] + + org = await orgs.find_one({"org_id": org_id}) + + + name_in_disk = await upload_org_logo(logo_file) + + # update org + org = await orgs.update_one({"org_id": org_id}, {"$set": {"logo": name_in_disk}}) + + return {"detail": "Logo updated"} + + + async def delete_org(request: Request, org_id: str, current_user: PublicUser): - await verify_org_rights(request, org_id, current_user, "delete") orgs = request.app.db["organizations"] @@ -131,7 +138,8 @@ async def delete_org(request: Request, org_id: str, current_user: PublicUser): if not org: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist") + status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist" + ) isDeleted = await orgs.delete_one({"org_id": org_id}) @@ -143,56 +151,80 @@ async def delete_org(request: Request, org_id: str, current_user: PublicUser): return {"detail": "Org deleted"} else: raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Unavailable database") + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Unavailable database", + ) -async def get_orgs_by_user(request: Request, user_id: str, page: int = 1, limit: int = 10): +async def get_orgs_by_user( + request: Request, user_id: str, page: int = 1, limit: int = 10 +): orgs = request.app.db["organizations"] user = request.app.db["users"] if user_id == "anonymous": - - # raise error + # raise error raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="User not logged in") - + status_code=status.HTTP_409_CONFLICT, detail="User not logged in" + ) + # get user orgs user_orgs = await user.find_one({"user_id": user_id}) org_ids: list[UserOrganization] = [] for org in user_orgs["orgs"]: - if org["org_role"] == "owner" or org["org_role"] == "editor" or org["org_role"] == "member": + if ( + org["org_role"] == "owner" + or org["org_role"] == "editor" + or org["org_role"] == "member" + ): org_ids.append(org["org_id"]) # find all orgs where org_id is in org_ids array - all_orgs = orgs.find({"org_id": {"$in": org_ids}}).sort( - "name", 1).skip(10 * (page - 1)).limit(100) + all_orgs = ( + orgs.find({"org_id": {"$in": org_ids}}) + .sort("name", 1) + .skip(10 * (page - 1)) + .limit(100) + ) - return [json.loads(json.dumps(org, default=str)) for org in await all_orgs.to_list(length=100)] + return [ + json.loads(json.dumps(org, default=str)) + for org in await all_orgs.to_list(length=100) + ] #### Security #################################################### -async def verify_org_rights(request: Request, org_id: str, current_user: PublicUser, action: str,): + +async def verify_org_rights( + request: Request, + org_id: str, + current_user: PublicUser, + action: str, +): orgs = request.app.db["organizations"] org = await orgs.find_one({"org_id": org_id}) if not org: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist") + status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist" + ) - # check if is owner of org - # todo check if is admin of org + hasRoleRights = await verify_user_rights_with_roles( + request, action, current_user.user_id, org_id, org_id + ) - hasRoleRights = await verify_user_rights_with_roles(request, action, current_user.user_id, org_id, org_id) - - # if not hasRoleRights and not isOwner: - # raise HTTPException( - # status_code=status.HTTP_403_FORBIDDEN, detail="You do not have rights to this organization") + if not hasRoleRights: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You do not have rights to this organization", + ) return True + #### Security #################################################### diff --git a/src/services/orgs/schemas/__init__.py b/src/services/orgs/schemas/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/services/orgs/schemas/orgs.py b/src/services/orgs/schemas/orgs.py new file mode 100644 index 00000000..82da07e2 --- /dev/null +++ b/src/services/orgs/schemas/orgs.py @@ -0,0 +1,29 @@ +from typing import Optional +from pydantic import BaseModel +from src.security.security import * + +#### Classes #################################################### + + +class Organization(BaseModel): + name: str + description: str + email: str + slug: str + logo: Optional[str] + default: Optional[bool] = False + + +class OrganizationInDB(Organization): + org_id: str + + +class PublicOrganization(Organization): + name: str + description: str + email: str + slug: str + org_id: str + + def __getitem__(self, item): + return getattr(self, item) diff --git a/src/services/trail.py b/src/services/trail.py index 956ec4e8..bb4f88eb 100644 --- a/src/services/trail.py +++ b/src/services/trail.py @@ -4,7 +4,7 @@ from uuid import uuid4 from fastapi import HTTPException, Request, status from pydantic import BaseModel from src.services.courses.chapters import get_coursechapters_meta -from src.services.orgs import PublicOrganization +from src.services.orgs.orgs import PublicOrganization from src.services.users.users import PublicUser