diff --git a/app.py b/app.py index a09626a9..c4f356a2 100644 --- a/app.py +++ b/app.py @@ -8,7 +8,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles from fastapi_jwt_auth.exceptions import AuthJWTException -from src.services.mocks.initial import create_initial_data +# from src.services.mocks.initial import create_initial_data ######################## # Pre-Alpha Version 0.1.0 @@ -64,8 +64,8 @@ async def root(): return {"Message": "Welcome to LearnHouse ✨"} -@app.get("/initial_data") -async def initial_data(request: Request): +# @app.get("/initial_data") +# async def initial_data(request: Request): - await create_initial_data(request) - return {"Message": "Initial data created 🤖"} +# await create_initial_data(request) +# return {"Message": "Initial data created 🤖"} diff --git a/front/app/_orgs/[orgslug]/(withmenu)/course/[courseid]/edit/page.tsx b/front/app/_orgs/[orgslug]/(withmenu)/course/[courseid]/edit/page.tsx index 55ec892d..ebf1fd6b 100644 --- a/front/app/_orgs/[orgslug]/(withmenu)/course/[courseid]/edit/page.tsx +++ b/front/app/_orgs/[orgslug]/(withmenu)/course/[courseid]/edit/page.tsx @@ -13,6 +13,7 @@ import { useRouter } from "next/navigation"; import NewChapterModal from "@components/Modals/CourseEdit/NewChapter"; import NewLectureModal from "@components/Modals/CourseEdit/NewLecture"; import { createLecture, createFileLecture } from "@services/courses/lectures"; +import { getOrganizationContextInfo } from "@services/orgs"; function CourseEdit(params: any) { const router = useRouter(); @@ -31,6 +32,8 @@ function CourseEdit(params: any) { const courseid = params.params.courseid; const orgslug = params.params.orgslug; + + async function getCourseChapters() { const courseChapters = await getCourseChaptersMetadata(courseid); setData(courseChapters); @@ -75,8 +78,9 @@ function CourseEdit(params: any) { // Submit new lecture const submitLecture = async (lecture: any) => { console.log("submitLecture", lecture); + let org = await getOrganizationContextInfo(orgslug); await updateChaptersMetadata(courseid, data); - await createLecture(lecture, lecture.chapterId); + await createLecture(lecture, lecture.chapterId, org.org_id); await getCourseChapters(); setNewLectureModal(false); }; @@ -226,63 +230,63 @@ function CourseEdit(params: any) { return ( <> - - - Edit Course {" "} - <button - onClick={() => { - setNewChapterModal(true); - }} - > - Add chapter + - </button> - <button - onClick={() => { - updateChapters(); - }} - > - Save - </button> - - {newChapterModal && } - {newLectureModal && ( - - )} + + + Edit Course {" "} + <button + onClick={() => { + setNewChapterModal(true); + }} + > + Add chapter + + </button> + <button + onClick={() => { + updateChapters(); + }} + > + Save + </button> + + {newChapterModal && } + {newLectureModal && ( + + )} -
- {winReady && ( - - - - {(provided) => ( - <> -
- {getChapters().map((info: any, index: any) => ( - <> - - - ))} - {provided.placeholder} -
- - )} -
-
-
- )} +
+ {winReady && ( + + + + {(provided) => ( + <> +
+ {getChapters().map((info: any, index: any) => ( + <> + + + ))} + {provided.placeholder} +
+ + )} +
+
+
+ )}
); diff --git a/front/app/_orgs/[orgslug]/(withmenu)/courses/new/page.tsx b/front/app/_orgs/[orgslug]/(withmenu)/courses/new/page.tsx index a05ad43c..8acc5a8a 100644 --- a/front/app/_orgs/[orgslug]/(withmenu)/courses/new/page.tsx +++ b/front/app/_orgs/[orgslug]/(withmenu)/courses/new/page.tsx @@ -1,11 +1,12 @@ "use client"; import { useRouter } from "next/navigation"; import React from "react"; -import { Header } from "../../../../../components/UI/Header"; -import Layout from "../../../../../components/UI/Layout"; -import { Title } from "../../../../../components/UI/Elements/Styles/Title"; -import { createNewCourse } from "../../../../../services/courses/courses"; -import { getOrganizationContextInfo } from "../../../../../services/orgs"; +import { Header } from "@components/UI/Header"; +import Layout from "@components/UI/Layout"; +import { Title } from "@components/UI/Elements/Styles/Title"; +import { createNewCourse } from "@services/courses/courses"; +import { getOrganizationContextInfo } from "@services/orgs"; +import { getUriWithOrg } from "@services/config"; const NewCoursePage = (params: any) => { const router = useRouter(); @@ -40,7 +41,7 @@ const NewCoursePage = (params: any) => { // TODO : wow this is terrible - fix this if (status.org_id == orgId) { - router.push(`/org/${orgslug}/courses`); + router.push(getUriWithOrg(orgslug, `/courses`)); } else { alert("Error creating course, please see console logs"); console.log(status); diff --git a/front/services/courses/lectures.ts b/front/services/courses/lectures.ts index 2f961f09..4c20f9fd 100644 --- a/front/services/courses/lectures.ts +++ b/front/services/courses/lectures.ts @@ -1,13 +1,13 @@ import { getAPIUrl } from "@services/config"; import { RequestBody, RequestBodyForm } from "@services/utils/requests"; -export async function createLecture(data: any, chapter_id: any) { +export async function createLecture(data: any, chapter_id: any, org_id: any) { data.content = {}; - // remove chapter_id from data delete data.chapterId; + - const result: any = await fetch(`${getAPIUrl()}lectures/?coursechapter_id=${chapter_id}`, RequestBody("POST", data)) + const result: any = await fetch(`${getAPIUrl()}lectures/?coursechapter_id=${chapter_id}&org_id=${org_id}`, RequestBody("POST", data)) .then((result) => result.json()) .catch((error) => console.log("error", error)); diff --git a/src/dependencies/auth.py b/src/dependencies/auth.py index 92777f4f..1e3f0c02 100644 --- a/src/dependencies/auth.py +++ b/src/dependencies/auth.py @@ -4,7 +4,7 @@ from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from passlib.context import CryptContext from jose import JWTError, jwt from datetime import datetime, timedelta -from src.services.users import * +from src.services.users.users import * from fastapi import Cookie, FastAPI from src.services.security import * from fastapi_jwt_auth import AuthJWT diff --git a/src/routers/auth.py b/src/routers/auth.py index d08ef3ff..9c8bf905 100644 --- a/src/routers/auth.py +++ b/src/routers/auth.py @@ -2,7 +2,7 @@ from urllib.request import Request from fastapi import Depends, APIRouter, HTTPException, status, Request from fastapi.security import OAuth2PasswordRequestForm from src.dependencies.auth import * -from src.services.users import * +from src.services.users.users import * from datetime import timedelta from fastapi.responses import JSONResponse diff --git a/src/routers/blocks.py b/src/routers/blocks.py index b9b68707..9103e587 100644 --- a/src/routers/blocks.py +++ b/src/routers/blocks.py @@ -5,7 +5,7 @@ from src.services.blocks.imageBlock.images import create_image_file, get_image_f from src.services.blocks.videoBlock.videos import create_video_file, get_video_file from src.services.blocks.pdfBlock.documents import create_document_file, get_document_file from src.services.blocks.quizBlock.quizBlock import create_quiz_block, get_quiz_block_answers, get_quiz_block_options, quizBlock -from src.services.users import PublicUser +from src.services.users.users import PublicUser router = APIRouter() diff --git a/src/routers/courses/chapters.py b/src/routers/courses/chapters.py index a00b7b83..57d42fec 100644 --- a/src/routers/courses/chapters.py +++ b/src/routers/courses/chapters.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, Depends, Request, UploadFile, Form from src.services.courses.chapters import CourseChapter, CourseChapterMetaData, create_coursechapter, delete_coursechapter, get_coursechapter, get_coursechapters, get_coursechapters_meta, update_coursechapter, update_coursechapters_meta -from src.services.users import PublicUser +from src.services.users.users import PublicUser from src.dependencies.auth import get_current_user router = APIRouter() diff --git a/src/routers/courses/collections.py b/src/routers/courses/collections.py index 284b7cad..26293524 100644 --- a/src/routers/courses/collections.py +++ b/src/routers/courses/collections.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, Depends, Request from src.dependencies.auth import get_current_user -from src.services.users import PublicUser, User +from src.services.users.users import PublicUser, User from src.services.courses.collections import Collection, create_collection, get_collection, get_collections, update_collection, delete_collection diff --git a/src/routers/courses/courses.py b/src/routers/courses/courses.py index 340cb735..f15b015d 100644 --- a/src/routers/courses/courses.py +++ b/src/routers/courses/courses.py @@ -2,14 +2,14 @@ from fastapi import APIRouter, Depends, UploadFile, Form, Request from src.dependencies.auth import get_current_user from src.services.courses.courses import Course, create_course, get_course, get_course_meta, get_courses, get_courses_orgslug, update_course, delete_course, update_course_thumbnail -from src.services.users import PublicUser +from src.services.users.users import PublicUser router = APIRouter() @router.post("/") -async def api_create_course(request: Request,org_id: str, name: str = Form(), mini_description: str = Form(), description: str = Form(), public: bool = Form(), current_user: PublicUser = Depends(get_current_user), thumbnail: UploadFile | None = None): +async def api_create_course(request: Request, org_id: str, name: str = Form(), mini_description: str = Form(), description: str = Form(), public: bool = Form(), current_user: PublicUser = Depends(get_current_user), thumbnail: UploadFile | None = None): """ Create new Course """ @@ -19,7 +19,7 @@ async def api_create_course(request: Request,org_id: str, name: str = Form(), mi @router.put("/thumbnail/{course_id}") -async def api_create_course_thumbnail(request: Request,course_id: str, thumbnail: UploadFile | None = None, current_user: PublicUser = Depends(get_current_user)): +async def api_create_course_thumbnail(request: Request, course_id: str, thumbnail: UploadFile | None = None, current_user: PublicUser = Depends(get_current_user)): """ Update new Course Thumbnail """ @@ -27,7 +27,7 @@ async def api_create_course_thumbnail(request: Request,course_id: str, thumbnail @router.get("/{course_id}") -async def api_get_course(request: Request,course_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_get_course(request: Request, course_id: str, current_user: PublicUser = Depends(get_current_user)): """ Get single Course by course_id """ @@ -35,7 +35,7 @@ async def api_get_course(request: Request,course_id: str, current_user: PublicU @router.get("/meta/{course_id}") -async def api_get_course_meta(request: Request,course_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_get_course_meta(request: Request, course_id: str, current_user: PublicUser = Depends(get_current_user)): """ Get single Course Metadata (chapters, lectures) by course_id """ @@ -43,30 +43,31 @@ async def api_get_course_meta(request: Request,course_id: str, current_user: Pu @router.get("/org_id/{org_id}/page/{page}/limit/{limit}") -async def api_get_course_by(request: Request,page: int, limit: int, org_id: str): +async def api_get_course_by(request: Request, page: int, limit: int, org_id: str): """ Get houses by page and limit """ - return await get_courses(request,page, limit, org_id) + return await get_courses(request, page, limit, org_id) + @router.get("/org_slug/{org_slug}/page/{page}/limit/{limit}") -async def api_get_course_by_orgslug(request: Request,page: int, limit: int, org_slug: str): +async def api_get_course_by_orgslug(request: Request, page: int, limit: int, org_slug: str): """ Get houses by page and limit """ - return await get_courses_orgslug(request,page, limit, org_slug) + return await get_courses_orgslug(request, page, limit, org_slug) @router.put("/{course_id}") -async def api_update_course(request: Request,course_object: Course, course_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_update_course(request: Request, course_object: Course, course_id: str, current_user: PublicUser = Depends(get_current_user)): """ Update Course by course_id """ - return await update_course(request,course_object, course_id, current_user) + return await update_course(request, course_object, course_id, current_user) @router.delete("/{course_id}") -async def api_delete_course(request: Request,course_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_delete_course(request: Request, course_id: str, current_user: PublicUser = Depends(get_current_user)): """ Delete Course by ID """ diff --git a/src/routers/courses/lectures.py b/src/routers/courses/lectures.py index bf26a8e9..aed582de 100644 --- a/src/routers/courses/lectures.py +++ b/src/routers/courses/lectures.py @@ -7,15 +7,15 @@ router = APIRouter() @router.post("/") -async def api_create_lecture(request: Request,lecture_object: Lecture, coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_create_lecture(request: Request, lecture_object: Lecture, org_id: str, coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)): """ Create new lecture """ - return await create_lecture(request,lecture_object, coursechapter_id, current_user) + return await create_lecture(request, lecture_object, org_id, coursechapter_id, current_user) @router.get("/{lecture_id}") -async def api_get_lecture(request: Request,lecture_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_get_lecture(request: Request, lecture_id: str, current_user: PublicUser = Depends(get_current_user)): """ Get single lecture by lecture_id """ @@ -23,32 +23,34 @@ async def api_get_lecture(request: Request,lecture_id: str, current_user: Public @router.get("/coursechapter/{coursechapter_id}") -async def api_get_lectures(request: Request,coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_get_lectures(request: Request, coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)): """ Get CourseChapter lectures """ - return await get_lectures(request,coursechapter_id, current_user) + return await get_lectures(request, coursechapter_id, current_user) @router.put("/{lecture_id}") -async def api_update_lecture(request: Request,lecture_object: Lecture, lecture_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_update_lecture(request: Request, lecture_object: Lecture, lecture_id: str, current_user: PublicUser = Depends(get_current_user)): """ Update lecture by lecture_id """ - return await update_lecture(request,lecture_object, lecture_id, current_user) + return await update_lecture(request, lecture_object, lecture_id, current_user) @router.delete("/{lecture_id}") -async def api_delete_lecture(request: Request,lecture_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_delete_lecture(request: Request, lecture_id: str, org_id: str, current_user: PublicUser = Depends(get_current_user)): """ Delete lecture by lecture_id """ - return await delete_lecture(request,lecture_id, current_user) + return await delete_lecture(request, lecture_id, current_user) # Video play + + @router.post("/video") -async def api_create_video_lecture(request: Request,name: str = Form(), coursechapter_id: str = Form(), current_user: PublicUser = Depends(get_current_user), video_file: UploadFile | None = None): +async def api_create_video_lecture(request: Request, org_id: str, name: str = Form(), coursechapter_id: str = Form(), current_user: PublicUser = Depends(get_current_user), video_file: UploadFile | None = None): """ Create new lecture """ - return await create_video_lecture(request,name, coursechapter_id, current_user, video_file) + return await create_video_lecture(request, name, coursechapter_id, current_user, video_file) diff --git a/src/routers/houses.py b/src/routers/houses.py index 164d5417..8827402f 100644 --- a/src/routers/houses.py +++ b/src/routers/houses.py @@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, Request from src.dependencies.auth import get_current_user from src.services.houses import House, HouseInDB, create_house, get_house, get_houses, update_house, delete_house -from src.services.users import PublicUser, User +from src.services.users.users import PublicUser, User router = APIRouter() diff --git a/src/routers/orgs.py b/src/routers/orgs.py index 465a953b..e571195e 100644 --- a/src/routers/orgs.py +++ b/src/routers/orgs.py @@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, Request from src.dependencies.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.users import PublicUser, User +from src.services.users.users import PublicUser, User router = APIRouter() diff --git a/src/routers/roles.py b/src/routers/roles.py index 5efebe00..903125e3 100644 --- a/src/routers/roles.py +++ b/src/routers/roles.py @@ -1,14 +1,15 @@ from fastapi import APIRouter, Depends, Request from src.dependencies.auth import get_current_user -from src.services.roles import Role, create_role, delete_role, get_role, get_roles, update_role -from src.services.users import PublicUser, User +from src.services.roles.schemas.roles import Role +from src.services.roles.roles import create_role, delete_role, read_role, update_role +from src.services.users.schemas.users import PublicUser, User router = APIRouter() @router.post("/") -async def api_create_role(request: Request,role_object: Role, current_user: PublicUser = Depends(get_current_user)): +async def api_create_role(request: Request, role_object: Role, current_user: PublicUser = Depends(get_current_user)): """ Create new role """ @@ -16,31 +17,23 @@ async def api_create_role(request: Request,role_object: Role, current_user: Publ @router.get("/{role_id}") -async def api_get_role(request: Request,role_id: str): +async def api_get_role(request: Request, role_id: str, current_user: PublicUser = Depends(get_current_user)): """ Get single role by role_id """ - return await get_role(request, role_id) - - -@router.get("/page/{page}/limit/{limit}") -async def api_get_role_by(request: Request,page: int, limit: int): - """ - Get roles by page and limit - """ - return await get_roles(request, page, limit) + return await read_role(request, role_id, current_user) @router.put("/{role_id}") -async def api_update_role(request: Request,role_object: Role, role_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_update_role(request: Request, role_object: Role, role_id: str, current_user: PublicUser = Depends(get_current_user)): """ Update role by role_id """ - return await update_role(request, role_object, role_id, current_user) + return await update_role(request, role_id, role_object, current_user) @router.delete("/{role_id}") -async def api_delete_role(request: Request,role_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_delete_role(request: Request, role_id: str, current_user: PublicUser = Depends(get_current_user)): """ Delete role by ID """ diff --git a/src/routers/users.py b/src/routers/users.py index 6a600bd5..8d79b7d0 100644 --- a/src/routers/users.py +++ b/src/routers/users.py @@ -2,7 +2,9 @@ from fastapi import Depends, FastAPI, APIRouter from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from pydantic import BaseModel from src.dependencies.auth import * -from src.services.users import * +from src.services.users.schemas.users import PasswordChangeForm, PublicUser, User, UserWithPassword +from src.services.users.users import create_user, delete_user, get_profile_metadata, get_user_by_userid, read_user, update_user, update_user_password + @@ -25,13 +27,6 @@ async def api_get_current_user_metadata(request: Request,current_user: User = De return await get_profile_metadata(request , current_user.dict()) -@router.get("/username/{username}") -async def api_get_user_by_username(request: Request, username: str): - """ - Get single user by username - """ - return await get_user(request, username) - @router.get("/user_id/{user_id}") async def api_get_user_by_userid(request: Request,user_id: str): @@ -42,32 +37,32 @@ async def api_get_user_by_userid(request: Request,user_id: str): @router.post("/") -async def api_create_user(request: Request,user_object: UserWithPassword): +async def api_create_user(request: Request,user_object: UserWithPassword, org_id: str ): """ Create new user """ - return await create_user(request, user_object) + return await create_user(request, None, user_object, org_id) @router.delete("/user_id/{user_id}") -async def api_delete_user(request: Request, user_id: str): +async def api_delete_user(request: Request, user_id: str, current_user: PublicUser = Depends(get_current_user)): """ Delete user by ID """ - return await delete_user(request, user_id) + return await delete_user(request, current_user, user_id) @router.put("/user_id/{user_id}") -async def api_update_user(request: Request, user_object: User, user_id: str): +async def api_update_user(request: Request, user_object: User, user_id: str, current_user: PublicUser = Depends(get_current_user)): """ Update user by ID """ - return await update_user(request, user_id, user_object) + return await update_user(request, user_id, user_object, current_user) @router.put("/password/user_id/{user_id}") -async def api_update_user_password(request: Request, user_id: str , passwordChangeForm : PasswordChangeForm): +async def api_update_user_password(request: Request, user_id: str , passwordChangeForm : PasswordChangeForm, current_user: PublicUser = Depends(get_current_user)): """ Update user password by ID """ - return await update_user_password(request, user_id, passwordChangeForm) + return await update_user_password(request,current_user, user_id, passwordChangeForm) diff --git a/src/services/activity.py b/src/services/activity.py index ae2ece70..88d85d17 100644 --- a/src/services/activity.py +++ b/src/services/activity.py @@ -7,7 +7,7 @@ from fastapi import HTTPException, Request, status from pydantic import BaseModel from src.services.courses.chapters import get_coursechapters_meta -from src.services.users import PublicUser +from src.services.users.users import PublicUser #### Classes #################################################### diff --git a/src/services/blocks/imageBlock/images.py b/src/services/blocks/imageBlock/images.py index be8104b9..0ceba3bf 100644 --- a/src/services/blocks/imageBlock/images.py +++ b/src/services/blocks/imageBlock/images.py @@ -4,7 +4,7 @@ from fastapi import HTTPException, status, UploadFile, Request from fastapi.responses import StreamingResponse import os -from src.services.users import PublicUser +from src.services.users.users import PublicUser class PhotoFile(BaseModel): diff --git a/src/services/blocks/pdfBlock/documents.py b/src/services/blocks/pdfBlock/documents.py index 0f061ecd..eeb861ea 100644 --- a/src/services/blocks/pdfBlock/documents.py +++ b/src/services/blocks/pdfBlock/documents.py @@ -4,7 +4,7 @@ from fastapi import HTTPException, status, UploadFile, Request from fastapi.responses import StreamingResponse import os -from src.services.users import PublicUser +from src.services.users.users import PublicUser class DocumentFile(BaseModel): diff --git a/src/services/blocks/quizBlock/quizBlock.py b/src/services/blocks/quizBlock/quizBlock.py index d1f0cecb..1d5e43fa 100644 --- a/src/services/blocks/quizBlock/quizBlock.py +++ b/src/services/blocks/quizBlock/quizBlock.py @@ -3,7 +3,7 @@ from uuid import uuid4 from fastapi import Request from pydantic import BaseModel from src.services.blocks.blocks import Block -from src.services.users import PublicUser +from src.services.users.users import PublicUser class option(BaseModel): diff --git a/src/services/blocks/videoBlock/videos.py b/src/services/blocks/videoBlock/videos.py index b703013f..768d6adb 100644 --- a/src/services/blocks/videoBlock/videos.py +++ b/src/services/blocks/videoBlock/videos.py @@ -4,7 +4,7 @@ import os from fastapi import HTTPException, status, UploadFile,Request from fastapi.responses import StreamingResponse -from src.services.users import PublicUser +from src.services.users.users import PublicUser class VideoFile(BaseModel): diff --git a/src/services/courses/chapters.py b/src/services/courses/chapters.py index bda0aeff..3841dbab 100644 --- a/src/services/courses/chapters.py +++ b/src/services/courses/chapters.py @@ -6,7 +6,7 @@ from pydantic import BaseModel from src.services.courses.courses import Course, CourseInDB from src.services.courses.lectures.lectures import Lecture, LectureInDB from src.services.security import verify_user_rights_with_roles -from src.services.users import PublicUser +from src.services.users.users import PublicUser from fastapi import HTTPException, status, Request, Response, BackgroundTasks, UploadFile, File @@ -40,10 +40,13 @@ async def create_coursechapter(request: Request,coursechapter_object: CourseChap coursechapters = request.app.db["coursechapters"] courses = request.app.db["courses"] + # get course org_id and verify rights + course = await courses.find_one({"course_id": course_id}) + # generate coursechapter_id with uuid4 coursechapter_id = str(f"coursechapter_{uuid4()}") - hasRoleRights = await verify_user_rights_with_roles(request, "create", current_user.user_id, coursechapter_id) + hasRoleRights = await verify_user_rights_with_roles(request, "create", current_user.user_id, coursechapter_id, course["org_id"]) if not hasRoleRights: raise HTTPException( @@ -229,7 +232,7 @@ async def verify_rights(request: Request,course_id: str, current_user: PublicUse raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"Course does not exist") - hasRoleRights = await verify_user_rights_with_roles(request, action, current_user.user_id, course_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: diff --git a/src/services/courses/collections.py b/src/services/courses/collections.py index 3996c91b..63eca6f7 100644 --- a/src/services/courses/collections.py +++ b/src/services/courses/collections.py @@ -2,7 +2,7 @@ import json from typing import List from uuid import uuid4 from pydantic import BaseModel -from src.services.users import PublicUser, User +from src.services.users.users import PublicUser, User from src.services.security import * from fastapi import FastAPI, HTTPException, status, Request, Response, BackgroundTasks from datetime import datetime diff --git a/src/services/courses/courses.py b/src/services/courses/courses.py index 265dcf85..98b7343c 100644 --- a/src/services/courses/courses.py +++ b/src/services/courses/courses.py @@ -4,7 +4,7 @@ from uuid import uuid4 from pydantic import BaseModel from src.services.courses.lectures.lectures import LectureInDB from src.services.courses.thumbnails import upload_thumbnail -from src.services.users import PublicUser +from src.services.users.users import PublicUser from src.services.security import * from fastapi import HTTPException, status, UploadFile from datetime import datetime @@ -141,12 +141,9 @@ 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 - hasRoleRights = await verify_user_rights_with_roles(request, "create", current_user.user_id, course_id) - - if not hasRoleRights: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Roles : Insufficient rights to perform this action") + 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]}" await upload_thumbnail(thumbnail_file, name_in_disk) @@ -290,7 +287,7 @@ async def verify_rights(request: Request, course_id: str, current_user: PublicUs raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"Course/CourseChapter does not exist") - hasRoleRights = await verify_user_rights_with_roles(request, action, current_user.user_id, course_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: diff --git a/src/services/courses/lectures/lectures.py b/src/services/courses/lectures/lectures.py index 51d58fb4..4ac492f1 100644 --- a/src/services/courses/lectures/lectures.py +++ b/src/services/courses/lectures/lectures.py @@ -1,6 +1,6 @@ from pydantic import BaseModel from src.services.security import verify_user_rights_with_roles -from src.services.users import PublicUser, User +from src.services.users.schemas.users import PublicUser, User from fastapi import FastAPI, HTTPException, status, Request, Response, BackgroundTasks, UploadFile, File from uuid import uuid4 from datetime import datetime @@ -17,6 +17,7 @@ class Lecture(BaseModel): class LectureInDB(Lecture): lecture_id: str coursechapter_id: str + org_id: str creationDate: str updateDate: str @@ -28,14 +29,14 @@ class LectureInDB(Lecture): #################################################### -async def create_lecture(request: Request,lecture_object: Lecture, coursechapter_id: str, current_user: PublicUser): +async def create_lecture(request: Request, lecture_object: Lecture, org_id: str, coursechapter_id: str, current_user: PublicUser): lectures = request.app.db["lectures"] coursechapters = request.app.db["coursechapters"] # generate lecture_id lecture_id = str(f"lecture_{uuid4()}") - hasRoleRights = await verify_user_rights_with_roles(request, "create", current_user.user_id, lecture_id) + hasRoleRights = await verify_user_rights_with_roles(request, "create", current_user.user_id, lecture_id, org_id) if not hasRoleRights: raise HTTPException( @@ -43,23 +44,23 @@ async def create_lecture(request: Request,lecture_object: Lecture, coursechapter # create lecture lecture = LectureInDB(**lecture_object.dict(), creationDate=str( - datetime.now()), coursechapter_id=coursechapter_id, updateDate=str(datetime.now()), lecture_id=lecture_id) + datetime.now()), coursechapter_id=coursechapter_id, updateDate=str(datetime.now()), lecture_id=lecture_id, org_id=org_id) await lectures.insert_one(lecture.dict()) # update chapter await coursechapters.update_one({"coursechapter_id": coursechapter_id}, { - "$addToSet": {"lectures": lecture_id}}) + "$addToSet": {"lectures": lecture_id}}) return lecture -async def get_lecture(request: Request,lecture_id: str, current_user: PublicUser): +async def get_lecture(request: Request, lecture_id: str, current_user: PublicUser): lectures = request.app.db["lectures"] lecture = await lectures.find_one({"lecture_id": lecture_id}) # verify course rights - hasRoleRights = await verify_user_rights_with_roles(request,"read", current_user.user_id, lecture_id) + hasRoleRights = await verify_user_rights_with_roles(request, "read", current_user.user_id, lecture_id, element_org_id=lecture["org_id"]) if not hasRoleRights: raise HTTPException( @@ -73,14 +74,13 @@ async def get_lecture(request: Request,lecture_id: str, current_user: PublicUser return lecture -async def update_lecture(request: Request,lecture_object: Lecture, lecture_id: str, current_user: PublicUser): - - # verify course rights - await verify_user_rights_with_roles(request, "update", current_user.user_id, lecture_id) +async def update_lecture(request: Request, lecture_object: Lecture, lecture_id: str, current_user: PublicUser): lectures = request.app.db["lectures"] lecture = await lectures.find_one({"lecture_id": lecture_id}) + # verify course rights + await verify_user_rights_with_roles(request, "update", current_user.user_id, lecture_id, element_org_id=lecture["org_id"]) if lecture: creationDate = lecture["creationDate"] @@ -89,7 +89,7 @@ async def update_lecture(request: Request,lecture_object: Lecture, lecture_id: s datetime_object = datetime.now() updated_course = LectureInDB( - lecture_id=lecture_id, coursechapter_id=lecture["coursechapter_id"], creationDate=creationDate, updateDate=str(datetime_object), **lecture_object.dict()) + lecture_id=lecture_id, coursechapter_id=lecture["coursechapter_id"], creationDate=creationDate, updateDate=str(datetime_object), org_id=lecture["org_id"], **lecture_object.dict()) await lectures.update_one({"lecture_id": lecture_id}, { "$set": updated_course.dict()}) @@ -101,15 +101,15 @@ async def update_lecture(request: Request,lecture_object: Lecture, lecture_id: s status_code=status.HTTP_409_CONFLICT, detail="lecture does not exist") -async def delete_lecture(request: Request,lecture_id: str, current_user: PublicUser): - - # verify course rights - await verify_user_rights_with_roles(request,"delete", current_user.user_id, lecture_id) +async def delete_lecture(request: Request, lecture_id: str, current_user: PublicUser): lectures = request.app.db["lectures"] lecture = await lectures.find_one({"lecture_id": lecture_id}) + # verify course rights + await verify_user_rights_with_roles(request, "delete", current_user.user_id, lecture_id, element_org_id=lecture["org_id"]) + if not lecture: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="lecture does not exist") @@ -127,18 +127,19 @@ async def delete_lecture(request: Request,lecture_id: str, current_user: PublicU #################################################### -async def get_lectures(request: Request,coursechapter_id: str, current_user: PublicUser): +async def get_lectures(request: Request, coursechapter_id: str, current_user: PublicUser): lectures = request.app.db["lectures"] - # verify course rights - await verify_user_rights_with_roles(request,"read", current_user.user_id, coursechapter_id) + # TODO : TERRIBLE SECURITY ISSUE HERE, NEED TO FIX ASAP + # TODO : TERRIBLE SECURITY ISSUE HERE, NEED TO FIX ASAP + # TODO : TERRIBLE SECURITY ISSUE HERE, NEED TO FIX ASAP lectures = lectures.find({"coursechapter_id": coursechapter_id}) if not lectures: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Course does not exist") - + lectures = [LectureInDB(**lecture) for lecture in await lectures.to_list(length=100)] return lectures diff --git a/src/services/courses/lectures/uploads/videos.py b/src/services/courses/lectures/uploads/videos.py index 78106531..8d503100 100644 --- a/src/services/courses/lectures/uploads/videos.py +++ b/src/services/courses/lectures/uploads/videos.py @@ -13,7 +13,6 @@ async def upload_video(video_file, lecture_id): f.close() except Exception as e: - print(e) return {"message": "There was an error uploading the file"} finally: video_file.file.close() diff --git a/src/services/courses/lectures/video.py b/src/services/courses/lectures/video.py index b2c0dd0b..66726291 100644 --- a/src/services/courses/lectures/video.py +++ b/src/services/courses/lectures/video.py @@ -1,7 +1,7 @@ from pydantic import BaseModel from src.services.security import verify_user_rights_with_roles from src.services.courses.lectures.uploads.videos import upload_video -from src.services.users import PublicUser +from src.services.users.users import PublicUser from src.services.courses.lectures.lectures import LectureInDB from fastapi import HTTPException, status, UploadFile, Request from uuid import uuid4 @@ -48,9 +48,7 @@ async def create_video_lecture(request: Request,name: str, coursechapter_id: st # upload video if video_file: - print("uploading video") # get videofile format - await upload_video(video_file, lecture_id) # todo : choose whether to update the chapter or not diff --git a/src/services/courses/thumbnails.py b/src/services/courses/thumbnails.py index 1306038d..cef0d41c 100644 --- a/src/services/courses/thumbnails.py +++ b/src/services/courses/thumbnails.py @@ -9,7 +9,6 @@ async def upload_thumbnail(thumbnail_file, name_in_disk): f.close() except Exception as e: - print(e) return {"message": "There was an error uploading the file"} finally: thumbnail_file.file.close() \ No newline at end of file diff --git a/src/services/houses.py b/src/services/houses.py index 9acd1b96..5023302b 100644 --- a/src/services/houses.py +++ b/src/services/houses.py @@ -2,7 +2,7 @@ import json from typing import List from uuid import uuid4 from pydantic import BaseModel -from src.services.users import PublicUser, User +from src.services.users.users import PublicUser, User from src.services.security import * from fastapi import FastAPI, HTTPException, status, Request, Response, BackgroundTasks from datetime import datetime diff --git a/src/services/mocks/initial.py b/src/services/mocks/initial.py index dfbed6da..6785f344 100644 --- a/src/services/mocks/initial.py +++ b/src/services/mocks/initial.py @@ -1,186 +1,186 @@ -import requests -from datetime import datetime -from fileinput import filename -from pprint import pprint -from uuid import uuid4 -from fastapi import File, UploadFile, Request -from src.services.courses.chapters import CourseChapter, create_coursechapter -from src.services.courses.lectures.lectures import Lecture, create_lecture -from src.services.courses.thumbnails import upload_thumbnail -from src.services.users import PublicUser, User, UserInDB, UserWithPassword +# import requests +# from datetime import datetime +# from fileinput import filename +# from pprint import pprint +# from uuid import uuid4 +# from fastapi import File, UploadFile, Request +# from src.services.courses.chapters import CourseChapter, create_coursechapter +# from src.services.courses.lectures.lectures import Lecture, create_lecture +# from src.services.courses.thumbnails import upload_thumbnail +# from src.services.users.users import PublicUser, User, UserInDB, UserWithPassword -from src.services.orgs import OrganizationInDB, Organization, create_org -from src.services.roles import Permission, Elements, RolePolicy, create_role -from src.services.users import create_user -from src.services.courses.courses import Course, CourseInDB, create_course -from src.services.roles import Role -from faker import Faker +# from src.services.orgs import OrganizationInDB, Organization, create_org +# from src.services.roles.schemas.roles import Permission, Elements, RolePolicy, create_role +# from src.services.users.users import create_user +# from src.services.courses.courses import Course, CourseInDB, create_course +# from src.services.roles.roles import Role +# from faker import Faker -async def create_initial_data(request: Request): - fake = Faker(['en_US']) - fake_multilang = Faker( - ['en_US', 'de_DE', 'ja_JP', 'es_ES', 'it_IT', 'pt_BR', 'ar_PS']) +# async def create_initial_data(request: Request): +# fake = Faker(['en_US']) +# fake_multilang = Faker( +# ['en_US', 'de_DE', 'ja_JP', 'es_ES', 'it_IT', 'pt_BR', 'ar_PS']) - # Create users - ######################################## +# # Create users +# ######################################## - database_users = request.app.db["users"] - await database_users.delete_many({}) +# database_users = request.app.db["users"] +# await database_users.delete_many({}) - users = [] - admin_user = UserWithPassword( - username=f"admin", - email=f"admin@admin.admin", - password="admin", - user_type="isOwner", - ) +# users = [] +# admin_user = UserWithPassword( +# username=f"admin", +# email=f"admin@admin.admin", +# password="admin", +# user_type="isOwner", +# ) - admin_user = await create_user(request, admin_user) +# admin_user = await create_user(request, admin_user) - for i in range(0, 20): - user = UserWithPassword( - username=fake.simple_profile()['username'], - email=fake.email(), - password=fake.password(), - user_type="isOwner", - full_name=fake.name(), - ) - users.append(user) +# for i in range(0, 20): +# user = UserWithPassword( +# username=fake.simple_profile()['username'], +# email=fake.email(), +# password=fake.password(), +# user_type="isOwner", +# full_name=fake.name(), +# ) +# users.append(user) - for user in users: - await create_user(request, user) +# for user in users: +# await create_user(request, user) - # find admin user - users = request.app.db["users"] - admin_user = await users.find_one({"username": "admin"}) +# # find admin user +# users = request.app.db["users"] +# admin_user = await users.find_one({"username": "admin"}) - if admin_user: - admin_user = UserInDB(**admin_user) - current_user = PublicUser(**admin_user.dict()) - else: - raise Exception("Admin user not found") +# if admin_user: +# admin_user = UserInDB(**admin_user) +# current_user = PublicUser(**admin_user.dict()) +# else: +# raise Exception("Admin user not found") - # Create organizations - ######################################## +# # Create organizations +# ######################################## - database_orgs = request.app.db["organizations"] - await database_orgs.delete_many({}) +# database_orgs = request.app.db["organizations"] +# await database_orgs.delete_many({}) - organizations = [] - for i in range(0, 5): - company = fake.company() - # remove whitespace and special characters and make lowercase - slug = ''.join(e for e in company if e.isalnum()).lower() - org = Organization( - name=company, - description=fake.unique.text(), - email=fake.unique.email(), - slug=slug, - ) - organizations.append(org) - await create_org(request, org, current_user) +# organizations = [] +# for i in range(0, 5): +# company = fake.company() +# # remove whitespace and special characters and make lowercase +# slug = ''.join(e for e in company if e.isalnum()).lower() +# org = Organization( +# name=company, +# description=fake.unique.text(), +# email=fake.unique.email(), +# slug=slug, +# ) +# organizations.append(org) +# await create_org(request, org, current_user) - # Create roles - ######################################## +# # Create roles +# ######################################## - database_roles = request.app.db["roles"] - await database_roles.delete_many({}) +# database_roles = request.app.db["roles"] +# await database_roles.delete_many({}) - roles = [] - admin_role = Role( - name="admin", - description="admin", - policies=[RolePolicy(permissions=Permission( - action_create=True, - action_read=True, - action_update=True, - action_delete=True, - ), - elements=Elements( - courses=["*"], - users=["*"], - houses=["*"], - collections=["*"], - organizations=["*"], - coursechapters=["*"], - lectures=["*"], - ))], - linked_users=[admin_user.user_id], - ) - roles.append(admin_role) +# roles = [] +# admin_role = Role( +# name="admin", +# description="admin", +# policies=[RolePolicy(permissions=Permission( +# action_create=True, +# action_read=True, +# action_update=True, +# action_delete=True, +# ), +# elements=Elements( +# courses=["*"], +# users=["*"], +# houses=["*"], +# collections=["*"], +# organizations=["*"], +# coursechapters=["*"], +# lectures=["*"], +# ))], +# linked_users=[admin_user.user_id], +# ) +# roles.append(admin_role) - await create_role(request, admin_role, current_user) +# await create_role(request, admin_role, current_user) - # Generate Courses and CourseChapters - ######################################## +# # Generate Courses and CourseChapters +# ######################################## - database_courses = request.app.db["courses"] - database_chapters = request.app.db["coursechapters"] - await database_courses.delete_many({}) - await database_chapters.delete_many({}) +# database_courses = request.app.db["courses"] +# database_chapters = request.app.db["coursechapters"] +# await database_courses.delete_many({}) +# await database_chapters.delete_many({}) - courses = [] - orgs = request.app.db["organizations"] +# courses = [] +# orgs = request.app.db["organizations"] - if await orgs.count_documents({}) > 0: - for org in await orgs.find().to_list(length=100): - for i in range(0, 5): +# if await orgs.count_documents({}) > 0: +# for org in await orgs.find().to_list(length=100): +# for i in range(0, 5): - # get image in BinaryIO format from unsplash and save it to disk - image = requests.get( - "https://source.unsplash.com/random/800x600") - with open("thumbnail.jpg", "wb") as f: - f.write(image.content) +# # get image in BinaryIO format from unsplash and save it to disk +# image = requests.get( +# "https://source.unsplash.com/random/800x600") +# with open("thumbnail.jpg", "wb") as f: +# f.write(image.content) - course_id = f"course_{uuid4()}" - course = CourseInDB( - name=fake_multilang.unique.sentence(), - description=fake_multilang.unique.text(), - mini_description=fake_multilang.unique.text(), - thumbnail="thumbnail", - org_id=org['org_id'], - learnings=[fake_multilang.unique.sentence() - for i in range(0, 5)], - public=True, - chapters=[], - course_id=course_id, - creationDate=str(datetime.now()), - updateDate=str(datetime.now()), - authors=[current_user.user_id], - ) +# course_id = f"course_{uuid4()}" +# course = CourseInDB( +# name=fake_multilang.unique.sentence(), +# description=fake_multilang.unique.text(), +# mini_description=fake_multilang.unique.text(), +# thumbnail="thumbnail", +# org_id=org['org_id'], +# learnings=[fake_multilang.unique.sentence() +# for i in range(0, 5)], +# public=True, +# chapters=[], +# course_id=course_id, +# creationDate=str(datetime.now()), +# updateDate=str(datetime.now()), +# authors=[current_user.user_id], +# ) - courses = request.app.db["courses"] - name_in_disk = f"test_mock{course_id}.jpeg" +# courses = request.app.db["courses"] +# name_in_disk = f"test_mock{course_id}.jpeg" - image = requests.get( - "https://source.unsplash.com/random/800x600") - with open(f"content/uploads/img/{name_in_disk}", "wb") as f: - f.write(image.content) +# image = requests.get( +# "https://source.unsplash.com/random/800x600") +# with open(f"content/uploads/img/{name_in_disk}", "wb") as f: +# f.write(image.content) - course.thumbnail = name_in_disk +# course.thumbnail = name_in_disk - course = CourseInDB(**course.dict()) - course_in_db = await courses.insert_one(course.dict()) +# course = CourseInDB(**course.dict()) +# course_in_db = await courses.insert_one(course.dict()) - # create chapters - for i in range(0, 5): - coursechapter = CourseChapter( - name=fake_multilang.unique.sentence(), - description=fake_multilang.unique.text(), - lectures=[], - ) - coursechapter = await create_coursechapter(request,coursechapter, course_id, current_user) - pprint(coursechapter) - if coursechapter: - # create lectures - for i in range(0, 5): - lecture = Lecture( - name=fake_multilang.unique.sentence(), - type="dynamic", - content={}, - ) - lecture = await create_lecture(request,lecture, coursechapter['coursechapter_id'], current_user) +# # create chapters +# for i in range(0, 5): +# coursechapter = CourseChapter( +# name=fake_multilang.unique.sentence(), +# description=fake_multilang.unique.text(), +# lectures=[], +# ) +# coursechapter = await create_coursechapter(request,coursechapter, course_id, current_user) +# pprint(coursechapter) +# if coursechapter: +# # create lectures +# for i in range(0, 5): +# lecture = Lecture( +# name=fake_multilang.unique.sentence(), +# type="dynamic", +# content={}, +# ) +# lecture = await create_lecture(request,lecture, coursechapter['coursechapter_id'], current_user) diff --git a/src/services/orgs.py b/src/services/orgs.py index 1e45853d..d5576c98 100644 --- a/src/services/orgs.py +++ b/src/services/orgs.py @@ -2,7 +2,8 @@ import json from typing import List from uuid import uuid4 from pydantic import BaseModel -from src.services.users import PublicUser, User +from src.services.users.schemas.users import UserOrganization +from src.services.users.users import PublicUser, User from src.services.security import * from fastapi import FastAPI, HTTPException, status, Request, Response, BackgroundTasks from datetime import datetime @@ -62,6 +63,7 @@ async def get_organization_by_slug(request: Request, org_slug: str): async def create_org(request: Request, org_object: Organization, current_user: PublicUser): orgs = request.app.db["organizations"] + user = request.app.db["users"] # find if org already exists using name isOrgAvailable = await orgs.find_one({"slug": org_object.slug}) @@ -79,6 +81,13 @@ 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") + + # add org to user + await user.update_one({"user_id": current_user.user_id}, { + "$addToSet": {"orgs": user_organization.dict()}}) + if not org_in_db: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Unavailable database") @@ -125,6 +134,10 @@ async def delete_org(request: Request, org_id: str, current_user: PublicUser): isDeleted = await orgs.delete_one({"org_id": org_id}) + # remove org from all users + users = request.app.db["users"] + await users.update_many({}, {"$pull": {"orgs": {"org_id": org_id}}}) + if isDeleted: return {"detail": "Org deleted"} else: @@ -134,9 +147,20 @@ async def delete_org(request: Request, org_id: str, current_user: PublicUser): 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"] - # find all orgs where user_id is in owners or admins arrays - all_orgs = orgs.find({"$or": [{"owners": user_id}, {"admins": user_id}]}).sort( + # 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": + 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) return [json.loads(json.dumps(org, default=str)) for org in await all_orgs.to_list(length=100)] @@ -154,7 +178,7 @@ async def verify_org_rights(request: Request, org_id: str, current_user: Public status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist") isOwner = current_user.user_id in org["owners"] - hasRoleRights = await verify_user_rights_with_roles(request, action, current_user.user_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( diff --git a/src/services/roles.py b/src/services/roles.py deleted file mode 100644 index 2423392d..00000000 --- a/src/services/roles.py +++ /dev/null @@ -1,167 +0,0 @@ -import json -from typing import List -from uuid import uuid4 -from pydantic import BaseModel -from src.services.users import PublicUser, User -from src.services.security import * -from src.services.houses import House -from fastapi import HTTPException, status, Request -from datetime import datetime - -#### Classes #################################################### - - -class Permission(BaseModel): - action_create: bool - action_read: bool - action_update: bool - action_delete: bool - - -class Elements(BaseModel): - courses: List[str] - users: List[str] - houses: List[str] - collections: List[str] - organizations: List[str] - coursechapters: List[str] - lectures : List[str] - - -class RolePolicy(BaseModel): - permissions: Permission - elements: Elements - -class Role(BaseModel): - name: str - description: str - policies: List[RolePolicy] - linked_users: List[str] - -class RoleInDB(Role): - role_id: str - creationDate: str - updateDate: str - -#### Classes #################################################### - - -async def get_role(request: Request,role_id: str): - roles = request.app.db["roles"] - - role = await roles.find_one({"role_id": role_id}) - - if not role: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Role does not exist") - - role = Role(**role) - return role - - -async def create_role(request: Request,role_object: Role, current_user: PublicUser): - roles = request.app.db["roles"] - - # find if house already exists using name - isRoleAvailable = await roles.find_one({"name": role_object.name}) - - if isRoleAvailable: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Role name already exists") - - await verify_user_permissions_on_roles(request, "create", current_user) - - # generate house_id with uuid4 - role_id = str(f"role_{uuid4()}") - - role = RoleInDB(role_id=role_id, creationDate=str(datetime.now()), - updateDate=str(datetime.now()), **role_object.dict()) - - role_in_db = await roles.insert_one(role.dict()) - - if not role_in_db: - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Unavailable database") - - return role.dict() - - -async def update_role(request: Request,role_object: Role, role_id: str, current_user: PublicUser): - - # verify house rights - await verify_user_permissions_on_roles(request, "update", current_user) - - roles = request.app.db["roles"] - - role = await roles.find_one({"role_id": role_id}) - - if not role: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Role does not exist") - - updated_role = RoleInDB( - role_id=role_id, updateDate=str(datetime.now()), creationDate=role["creationDate"], **role_object.dict()) - - await roles.update_one({"role_id": role_id}, {"$set": updated_role.dict()}) - - return RoleInDB(**updated_role.dict()) - - -async def delete_role(request: Request,role_id: str, current_user: PublicUser): - - # verify house rights - await verify_user_permissions_on_roles(request, "delete", current_user) - - roles = request.app.db["roles"] - - role = await roles.find_one({"role_id": role_id}) - - if not role: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Role does not exist") - - isDeleted = await roles.delete_one({"role_id": role_id}) - - if isDeleted: - return {"detail": "Role deleted"} - else: - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Unavailable database") - - -async def get_roles(request: Request,page: int = 1, limit: int = 10): - roles = request.app.db["roles"] - - # get all roles from database - all_roles = roles.find().sort("name", 1).skip(10 * (page - 1)).limit(limit) - - return [json.loads(json.dumps(role, default=str)) for role in await all_roles.to_list(length=limit)] - - -#### Security #################################################### - -async def verify_user_permissions_on_roles(request: Request,action: str, current_user: PublicUser): - users = request.app.db["users"] - - user = await users.find_one({"user_id": current_user.user_id}) - - if not user: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="User does not exist") - - isOwner = "owner" in user["user_type"] - isEditor = "editor" in user["user_type"] - - # TODO: verify for all actions. - if action == "delete": - if isEditor: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail="You do not have rights to this Role") - - if not isOwner and not isEditor: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail="You do not have rights to this Role") - - return True - -#### Security #################################################### diff --git a/src/services/roles/__init__.py b/src/services/roles/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/services/roles/roles.py b/src/services/roles/roles.py new file mode 100644 index 00000000..cae2585c --- /dev/null +++ b/src/services/roles/roles.py @@ -0,0 +1,112 @@ +import json +from typing import List, Literal +from uuid import uuid4 +from pydantic import BaseModel +from src.services.roles.schemas.roles import Role, RoleInDB +from src.services.users.schemas.users import PublicUser, User +from src.services.security import * +from src.services.houses import House +from fastapi import HTTPException, status, Request +from datetime import datetime + + +async def create_role(request: Request, role_object: Role, current_user: PublicUser): + roles = request.app.db["roles"] + + await verify_user_permissions_on_roles(request, current_user, "create", None) + + + # create the role object in the database and return the object + role_id = "role_" + str(uuid4()) + + role = RoleInDB( + role_id=role_id, + created_at=str(datetime.now()), + updated_at=str(datetime.now()), + **role_object.dict() + ) + + await roles.insert_one(role.dict()) + + return role + +async def read_role(request: Request, role_id: str, current_user: PublicUser): + roles = request.app.db["roles"] + + await verify_user_permissions_on_roles(request, current_user, "read", role_id) + + role = RoleInDB(**await roles.find_one({"role_id": role_id})) + + return role + +async def update_role(request: Request, role_id: str, role_object: Role, current_user: PublicUser): + roles = request.app.db["roles"] + + await verify_user_permissions_on_roles(request, current_user, "update", role_id) + + role_object.updated_at = datetime.now() + + # Update the role object in the database and return the object + updated_role = RoleInDB(**await roles.find_one_and_update({"role_id": role_id}, {"$set": role_object.dict()}, return_document=True)) + + return updated_role + +async def delete_role(request: Request, role_id: str, current_user: PublicUser): + roles = request.app.db["roles"] + + await verify_user_permissions_on_roles(request, current_user, "delete", role_id) + + # Delete the role object in the database and return the object + deleted_role = RoleInDB(**await roles.find_one_and_delete({"role_id": role_id})) + + return deleted_role + +#### Security #################################################### + +async def verify_user_permissions_on_roles(request: Request, current_user: PublicUser, action: Literal["create", "read", "update", "delete"], role_id: str | None): + users = request.app.db["users"] + roles = request.app.db["roles"] + + # If current user is not authenticated + + if not current_user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Roles : Not authenticated") + + if action == "create": + if "owner" in [org.org_role for org in current_user.orgs]: + return True + + if role_id is not None: + role = RoleInDB(**await roles.find_one({"role_id": role_id})) + + if action == "read": + if "owner" in [org.org_role for org in current_user.orgs]: + return True + + for org in current_user.orgs: + if org.org_id == role.org_id: + return True + + if action == "update": + for org in current_user.orgs: + # If the user is an owner of the organization + if org.org_id == role.org_id: + if org.org_role == "owner" or org.org_role == "editor": + return True + # Can't update a global role + if role.org_id == "*": + return False + + if action == "delete": + for org in current_user.orgs: + # If the user is an owner of the organization + if org.org_id == role.org_id: + if org.org_role == "owner": + return True + # Can't delete a global role + if role.org_id == "*": + return False + + +#### Security #################################################### diff --git a/src/services/roles/schemas/__init__.py b/src/services/roles/schemas/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/services/roles/schemas/roles.py b/src/services/roles/schemas/roles.py new file mode 100644 index 00000000..cf3f5f74 --- /dev/null +++ b/src/services/roles/schemas/roles.py @@ -0,0 +1,41 @@ +from typing import List, Literal +from pydantic import BaseModel + + +# Database Models + +class Permission(BaseModel): + action_create: bool + action_read: bool + action_update: bool + action_delete: bool + + def __getitem__(self, item): + return getattr(self, item) + + +class Elements(BaseModel): + courses: Permission + users: Permission + houses: Permission + collections: Permission + organizations: Permission + coursechapters: Permission + lectures: Permission + + def __getitem__(self, item): + return getattr(self, item) + + +class Role(BaseModel): + name: str + description: str + elements : Elements + org_id: str | Literal["*"] + + +class RoleInDB(Role): + role_id: str + created_at: str + updated_at: str + diff --git a/src/services/security.py b/src/services/security.py index 0b5efafc..8ebe3b99 100644 --- a/src/services/security.py +++ b/src/services/security.py @@ -1,6 +1,10 @@ +from pprint import pprint from fastapi import HTTPException, status, Request 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 User, UserInDB ### 🔒 JWT ############################################################## @@ -27,34 +31,47 @@ async def security_verify_password(plain_password: str, hashed_password: str): ### 🔒 Roles checking ############################################################## -async def verify_user_rights_with_roles(request: Request,action: str, user_id: str, element_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 """ roles = request.app.db["roles"] + users = request.app.db["users"] - # find data where user_id is in linked_users or * is in linked_users - user_roles_cursor = roles.find({"$or": [{"linked_users": user_id}, {"linked_users": "*"}]}) + # Check if the user is an admin + user: UserInDB = UserInDB(**await users.find_one({"user_id": user_id})) - + # Organization roles verification + for org in user.orgs: + # TODO: Check if the org_id (user) is the same as the org_id (element) - user_roles = [] + if org.org_id == element_org_id: + return True - # Info: permission actions are: read, create, delete, update + # Check if user is owner or reader of the organization + if org.org_role == ("owner" or "editor"): + return True - for role in await user_roles_cursor.to_list(length=100): - user_roles.append(role) + # If the user is not an owner or a editor, check if he has a role + # Get user roles + user_roles = user.roles - for role in user_roles: - for policy in role['policies']: - element = policy["elements"][await check_element_type(element_id)] - permission_state = policy["permissions"][f'action_{action}'] + # TODO: Check if the org_id of the role is the same as the org_id of the element using find - ## - if ("*" in element or element_id in element) and (permission_state is True): - return True - else: - return False + 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 + + # 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") async def check_element_type(element_id): @@ -80,4 +97,29 @@ 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]): + + element_type = await check_element_type(element_id) + element = request.app.db[element_type] + roles = request.app.db["roles"] + + # get singular element type + 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 + + else: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail="You don't have the right to perform this action") + ### 🔒 Roles checking ############################################################## diff --git a/src/services/users.py b/src/services/users.py deleted file mode 100644 index ccc84ca8..00000000 --- a/src/services/users.py +++ /dev/null @@ -1,221 +0,0 @@ -from typing import Optional -from uuid import uuid4 -from pydantic import BaseModel -from src.services.security import * -from fastapi import HTTPException, status, Request -from datetime import datetime - -#### Classes #################################################### - - -class User(BaseModel): - username: str - email: str - full_name: str | None = None - disabled: bool | None = False - avatar_url: str | None = None - verified: bool | None = False - user_type: str | None = None - bio: str | None = None - -class UserWithPassword(User): - password: str - - -class PublicUser(User): - user_id: str - creationDate: str - updateDate: str - -class PasswordChangeForm(BaseModel): - old_password: str - new_password: str - - -class UserInDB(UserWithPassword): - user_id: str - password: str - creationDate: str - updateDate: str - - -class UserProfileMetadata(BaseModel): - user_object: PublicUser - roles = list - -# TODO : terrible, export role classes from one single source of truth - - -class Role(BaseModel): - name: str - description: str - permissions: object - elements: object - -#### Classes #################################################### - -# TODO : user actions security -# TODO : avatar upload and update - - -async def get_user(request: Request, username: str): - users = request.app.db["users"] - - user = await users.find_one({"username": username}) - - if not user: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="User does not exist") - - user = User(**user) - return user - - -async def get_profile_metadata(request: Request, user): - users = request.app.db["users"] - roles = request.app.db["roles"] - - user = await users.find_one({"user_id": user['user_id']}) - - if not user: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="User does not exist") - - # get roles - user_roles = roles.find({"linked_users": user['user_id']}) - - user_roles_list = [] - for role in await user_roles.to_list(length=100): - print(role) - user_roles_list.append(Role(**role)) - - return { - "user_object": PublicUser(**user), - "roles": user_roles_list - } - - -async def get_user_by_userid(request: Request, user_id: str): - users = request.app.db["users"] - - user = await users.find_one({"user_id": user_id}) - - if not user: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="User does not exist") - - user = User(**user) - return user - - -async def security_get_user(request: Request, email: str): - users = request.app.db["users"] - - user = await users.find_one({"email": email}) - - if not user: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="User with Email does not exist") - - return UserInDB(**user) - - -async def get_userid_by_username(request: Request, username: str): - users = request.app.db["users"] - - user = await users.find_one({"username": username}) - - if not user: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="User does not exist") - - return user["user_id"] - - -async def update_user(request: Request, user_id: str, user_object: User): - users = request.app.db["users"] - - isUserExists = await users.find_one({"user_id": user_id}) - isUsernameAvailable = await users.find_one({"username": user_object.username}) - - if not isUserExists: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="User does not exist") - - - # TODO : fix this - - # okay if username is not changed - if isUserExists["username"] == user_object.username: - user_object.username = user_object.username.lower() - - else: - if isUsernameAvailable: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Username already used") - - - updated_user = {"$set": user_object.dict()} - users.update_one({"user_id": user_id}, updated_user) - - return User(**user_object.dict()) - - -async def update_user_password(request: Request, user_id: str, password_change_form: PasswordChangeForm): - users = request.app.db["users"] - - isUserExists = await users.find_one({"user_id": user_id}) - - if not isUserExists: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="User does not exist") - - if not await security_verify_password(password_change_form.old_password, isUserExists["password"]): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail="Wrong password") - - new_password = await security_hash_password(password_change_form.new_password) - - updated_user = {"$set": {"password": new_password}} - users.update_one({"user_id": user_id}, updated_user) - - return {"detail": "Password updated"} - - -async def delete_user(request: Request, user_id: str): - users = request.app.db["users"] - - isUserAvailable = await users.find_one({"user_id": user_id}) - - if not isUserAvailable: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="User does not exist") - - users.delete_one({"user_id": user_id}) - - return {"detail": "User deleted"} - - -async def create_user(request: Request, user_object: UserWithPassword): - users = request.app.db["users"] - - isUserAvailable = await users.find_one({"username": user_object.username}) - - if isUserAvailable: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Username already exists") - - # generate house_id with uuid4 - user_id = str(f"user_{uuid4()}") - - # lowercase username - user_object.username = user_object.username.lower() - - user_object.password = await security_hash_password(user_object.password) - - user = UserInDB(user_id=user_id, creationDate=str(datetime.now()), - updateDate=str(datetime.now()), **user_object.dict()) - - user_in_db = await users.insert_one(user.dict()) - - return User(**user.dict()) diff --git a/src/services/users/__init__.py b/src/services/users/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/services/users/schemas/__init__.py b/src/services/users/schemas/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/services/users/schemas/users.py b/src/services/users/schemas/users.py new file mode 100644 index 00000000..974d1472 --- /dev/null +++ b/src/services/users/schemas/users.py @@ -0,0 +1,51 @@ +from typing import Literal +from pydantic import BaseModel + + +class UserOrganization(BaseModel): + org_id: str + org_role: Literal['owner', 'editor', 'member'] + + def __getitem__(self, item): + return getattr(self, item) + + +class User(BaseModel): + username: str + email: str + full_name: str | None = None + avatar_url: str | None = None + bio: str | None = None + + + +class UserWithPassword(User): + password: str + + +class UserInDB(User): + user_id: str + password: str + verified: bool | None = False + disabled: bool | None = False + orgs: list[UserOrganization] = [] + roles: list[str] = [] + creation_date: str + update_date: str + + + + +class PublicUser(User): + user_id: str + orgs: list[UserOrganization] = [] + roles: list[str] = [] + creation_date: str + update_date: str + + +# Forms #################################################### + +class PasswordChangeForm(BaseModel): + old_password: str + new_password: str diff --git a/src/services/users/users.py b/src/services/users/users.py new file mode 100644 index 00000000..7a4fdc6d --- /dev/null +++ b/src/services/users/users.py @@ -0,0 +1,243 @@ +from datetime import datetime +from typing import Literal +from uuid import uuid4 +from fastapi import HTTPException, Request, status +from src.services.roles.schemas.roles import Role +from src.services.security import security_hash_password, security_verify_password +from src.services.users.schemas.users import PasswordChangeForm, PublicUser, User, UserOrganization, UserWithPassword, UserInDB + + +async def create_user(request: Request, current_user: PublicUser | None, user_object: UserWithPassword, org_id: str): + users = request.app.db["users"] + + isUsernameAvailable = await users.find_one({"username": user_object.username}) + isEmailAvailable = await users.find_one({"email": user_object.email}) + + + if isUsernameAvailable: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Username already exists") + + if isEmailAvailable: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Email already exists") + + # Generate user_id with uuid4 + user_id = str(f"user_{uuid4()}") + + # Check if the requesting user is authenticated + if current_user is not None: + # Verify rights + await verify_user_rights_on_user(request, current_user, "create", user_id) + + # Set the username & hash the password + user_object.username = user_object.username.lower() + user_object.password = await security_hash_password(user_object.password) + + # Create initial orgs list with the org_id passed in + orgs = [UserOrganization(org_id=org_id, org_role="member")] + + # Give role + roles = ["role_1"] + + # Create the user + user = UserInDB(user_id=user_id, creation_date=str(datetime.now()), + update_date=str(datetime.now()), orgs=orgs, roles=roles, **user_object.dict()) + + # Insert the user into the database + await users.insert_one(user.dict()) + + return User(**user.dict()) + + +async def read_user(request: Request, current_user: PublicUser, user_id: str): + users = request.app.db["users"] + + # Check if the user exists + isUserExists = await users.find_one({"user_id": user_id}) + + # Verify rights + await verify_user_rights_on_user(request, current_user, "read", user_id) + + # If the user does not exist, raise an error + if not isUserExists: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="User does not exist") + + return User(**isUserExists) + + +async def update_user(request: Request, user_id: str, user_object: User,current_user: PublicUser): + users = request.app.db["users"] + + # Verify rights + await verify_user_rights_on_user(request, current_user, "update", user_id) + + isUserExists = await users.find_one({"user_id": user_id}) + isUsernameAvailable = await users.find_one({"username": user_object.username}) + isEmailAvailable = await users.find_one({"email": user_object.email}) + + if not isUserExists: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="User does not exist") + + # okay if username is not changed + if isUserExists["username"] == user_object.username: + user_object.username = user_object.username.lower() + + else: + if isUsernameAvailable: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Username already used") + + if isEmailAvailable: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Email already used") + + updated_user = {"$set": user_object.dict()} + users.update_one({"user_id": user_id}, updated_user) + + return User(**user_object.dict()) + + + +async def update_user_password(request: Request, current_user: PublicUser, user_id: str, password_change_form: PasswordChangeForm): + users = request.app.db["users"] + + isUserExists = await users.find_one({"user_id": user_id}) + + # Verify rights + await verify_user_rights_on_user(request, current_user, "update", user_id) + + if not isUserExists: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="User does not exist") + + if not await security_verify_password(password_change_form.old_password, isUserExists["password"]): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Wrong password") + + new_password = await security_hash_password(password_change_form.new_password) + + updated_user = {"$set": {"password": new_password}} + await users.update_one({"user_id": user_id}, updated_user) + + return {"detail": "Password updated"} + + +async def delete_user(request: Request, current_user: PublicUser, user_id: str): + users = request.app.db["users"] + + isUserExists = await users.find_one({"user_id": user_id}) + + # Verify is user has permission to delete the user + await verify_user_rights_on_user(request, current_user, "delete", user_id) + + if not isUserExists: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="User does not exist") + + await users.delete_one({"user_id": user_id}) + + return {"detail": "User deleted"} + + +# Utils & Security functions + +async def security_get_user(request: Request, email: str): + users = request.app.db["users"] + + + user = await users.find_one({"email": email}) + + if not user: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="User with Email does not exist") + + return UserInDB(**user) + +async def get_userid_by_username(request: Request, username: str): + users = request.app.db["users"] + + user = await users.find_one({"username": username}) + + if not user: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="User does not exist") + + return user["user_id"] + +async def get_user_by_userid(request: Request, user_id: str): + users = request.app.db["users"] + + user = await users.find_one({"user_id": user_id}) + + if not user: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="User does not exist") + + user = User(**user) + return user + +async def get_profile_metadata(request: Request, user): + users = request.app.db["users"] + roles = request.app.db["roles"] + + user = await users.find_one({"user_id": user['user_id']}) + + if not user: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="User does not exist") + + + + return { + "user_object": PublicUser(**user), + "roles": "random" + } + + +# Verification of the user's permissions on the roles + +async def verify_user_rights_on_user(request: Request, current_user: PublicUser, action: Literal["create", "read", "update", "delete"], user_id: str): + users = request.app.db["users"] + user = UserInDB(**await users.find_one({"user_id": user_id})) + + if action == "create": + return True + + if action == "read": + if current_user.user_id == user_id: + return True + + for org in current_user.orgs: + if org.org_id in [org.org_id for org in user.orgs]: + return True + + return False + + if action == "update": + if current_user.user_id == user_id: + return True + + for org in current_user.orgs: + if org.org_id in [org.org_id for org in user.orgs]: + + if org.org_role == "owner": + return True + + # TODO: Verify user roles on the org + + return False + + if action == "delete": + if current_user.user_id == user_id: + return True + + for org in current_user.orgs: + if org.org_id in [org.org_id for org in user.orgs]: + + if org.org_role == "owner": + return True + + # TODO: Verify user roles on the org