mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: refactor the entire learnhouse project
This commit is contained in:
parent
f556e41dda
commit
4c215e91d5
247 changed files with 7716 additions and 1013 deletions
0
apps/api/src/services/__init__.py
Normal file
0
apps/api/src/services/__init__.py
Normal file
0
apps/api/src/services/blocks/__init__.py
Normal file
0
apps/api/src/services/blocks/__init__.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
from uuid import uuid4
|
||||
from fastapi import HTTPException, status, UploadFile, Request
|
||||
from src.services.blocks.schemas.blocks import Block
|
||||
from src.services.blocks.utils.upload_files import upload_file_and_return_file_object
|
||||
|
||||
from src.services.users.users import PublicUser
|
||||
|
||||
|
||||
async def create_image_block(
|
||||
request: Request, image_file: UploadFile, activity_id: str
|
||||
):
|
||||
blocks = request.app.db["blocks"]
|
||||
activity = request.app.db["activities"]
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
block_type = "imageBlock"
|
||||
|
||||
# get org_id from activity
|
||||
activity = await activity.find_one({"activity_id": activity_id}, {"_id": 0})
|
||||
org_id = activity["org_id"]
|
||||
|
||||
coursechapter_id = activity["coursechapter_id"]
|
||||
|
||||
# get course_id from coursechapter
|
||||
course = await courses.find_one(
|
||||
{"chapters": coursechapter_id},
|
||||
{"_id": 0},
|
||||
)
|
||||
|
||||
|
||||
# get block id
|
||||
block_id = str(f"block_{uuid4()}")
|
||||
|
||||
block_data = await upload_file_and_return_file_object(
|
||||
request,
|
||||
image_file,
|
||||
activity_id,
|
||||
block_id,
|
||||
["jpg", "jpeg", "png", "gif"],
|
||||
block_type,
|
||||
org_id,
|
||||
course["course_id"],
|
||||
)
|
||||
|
||||
# create block
|
||||
block = Block(
|
||||
block_id=block_id,
|
||||
activity_id=activity_id,
|
||||
block_type=block_type,
|
||||
block_data=block_data,
|
||||
org_id=org_id,
|
||||
course_id=course["course_id"],
|
||||
)
|
||||
|
||||
# insert block
|
||||
await blocks.insert_one(block.dict())
|
||||
|
||||
return block
|
||||
|
||||
|
||||
async def get_image_block(request: Request, file_id: str, current_user: PublicUser):
|
||||
blocks = request.app.db["blocks"]
|
||||
|
||||
video_block = await blocks.find_one({"block_id": file_id})
|
||||
|
||||
if video_block:
|
||||
return Block(**video_block)
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Image block does not exist"
|
||||
)
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
from uuid import uuid4
|
||||
from fastapi import HTTPException, status, UploadFile, Request
|
||||
from src.services.blocks.schemas.blocks import Block
|
||||
from src.services.blocks.utils.upload_files import upload_file_and_return_file_object
|
||||
|
||||
from src.services.users.users import PublicUser
|
||||
|
||||
|
||||
async def create_pdf_block(request: Request, pdf_file: UploadFile, activity_id: str):
|
||||
blocks = request.app.db["blocks"]
|
||||
activity = request.app.db["activities"]
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
block_type = "pdfBlock"
|
||||
|
||||
# get org_id from activity
|
||||
activity = await activity.find_one({"activity_id": activity_id}, {"_id": 0})
|
||||
org_id = activity["org_id"]
|
||||
|
||||
# get block id
|
||||
block_id = str(f"block_{uuid4()}")
|
||||
|
||||
coursechapter_id = activity["coursechapter_id"]
|
||||
|
||||
# get course_id from coursechapter
|
||||
course = await courses.find_one(
|
||||
{"chapters": coursechapter_id},
|
||||
{"_id": 0},
|
||||
)
|
||||
|
||||
block_data = await upload_file_and_return_file_object(
|
||||
request,
|
||||
pdf_file,
|
||||
activity_id,
|
||||
block_id,
|
||||
["pdf"],
|
||||
block_type,
|
||||
org_id,
|
||||
course["course_id"],
|
||||
)
|
||||
|
||||
# create block
|
||||
block = Block(
|
||||
block_id=block_id,
|
||||
activity_id=activity_id,
|
||||
block_type=block_type,
|
||||
block_data=block_data,
|
||||
org_id=org_id,
|
||||
course_id=course["course_id"],
|
||||
)
|
||||
|
||||
# insert block
|
||||
await blocks.insert_one(block.dict())
|
||||
|
||||
return block
|
||||
|
||||
|
||||
async def get_pdf_block(request: Request, file_id: str, current_user: PublicUser):
|
||||
blocks = request.app.db["blocks"]
|
||||
|
||||
pdf_block = await blocks.find_one({"block_id": file_id})
|
||||
|
||||
if pdf_block:
|
||||
return Block(**pdf_block)
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Video file does not exist"
|
||||
)
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
from typing import List, Literal
|
||||
from uuid import uuid4
|
||||
from fastapi import Request
|
||||
from pydantic import BaseModel
|
||||
from src.services.blocks.schemas.blocks import Block
|
||||
from src.services.users.users import PublicUser
|
||||
|
||||
|
||||
class option(BaseModel):
|
||||
option_id: str
|
||||
option_type: Literal["text", "image"]
|
||||
option_data: str
|
||||
|
||||
|
||||
class answer(BaseModel):
|
||||
question_id: str
|
||||
option_id: str
|
||||
|
||||
|
||||
class question(BaseModel):
|
||||
question_id: str
|
||||
question_value:str
|
||||
options: List[option]
|
||||
|
||||
|
||||
class quizBlock(BaseModel):
|
||||
questions: List[question]
|
||||
answers: List[answer]
|
||||
|
||||
|
||||
async def create_quiz_block(request: Request, quizBlock: quizBlock, activity_id: str, user: PublicUser):
|
||||
blocks = request.app.db["blocks"]
|
||||
activities = request.app.db["activities"]
|
||||
request.app.db["courses"]
|
||||
|
||||
# Get org_id from activity
|
||||
activity = await activities.find_one({"activity_id": activity_id}, {"_id": 0, "org_id": 1})
|
||||
org_id = activity["org_id"]
|
||||
|
||||
# Get course_id from activity
|
||||
course = await activities.find_one({"activity_id": activity_id}, {"_id": 0, "course_id": 1})
|
||||
|
||||
block_id = str(f"block_{uuid4()}")
|
||||
|
||||
# create block
|
||||
block = Block(block_id=block_id, activity_id=activity_id,
|
||||
block_type="quizBlock", block_data=quizBlock, org_id=org_id, course_id=course["course_id"])
|
||||
|
||||
# insert block
|
||||
await blocks.insert_one(block.dict())
|
||||
|
||||
return block
|
||||
|
||||
|
||||
async def get_quiz_block_options(request: Request, block_id: str, user: PublicUser):
|
||||
blocks = request.app.db["blocks"]
|
||||
# find block but get only the options
|
||||
block = await blocks.find_one({"block_id": block_id, }, {
|
||||
"_id": 0, "block_data.answers": 0})
|
||||
|
||||
return block
|
||||
|
||||
async def get_quiz_block_answers(request: Request, block_id: str, user: PublicUser):
|
||||
blocks = request.app.db["blocks"]
|
||||
|
||||
# find block but get only the answers
|
||||
block = await blocks.find_one({"block_id": block_id, }, {
|
||||
"_id": 0, "block_data.questions": 0})
|
||||
|
||||
return block
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
from uuid import uuid4
|
||||
from fastapi import HTTPException, status, UploadFile, Request
|
||||
from src.services.blocks.schemas.blocks import Block
|
||||
from src.services.blocks.utils.upload_files import upload_file_and_return_file_object
|
||||
|
||||
from src.services.users.users import PublicUser
|
||||
|
||||
|
||||
async def create_video_block(
|
||||
request: Request, video_file: UploadFile, activity_id: str
|
||||
):
|
||||
blocks = request.app.db["blocks"]
|
||||
activity = request.app.db["activities"]
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
block_type = "videoBlock"
|
||||
|
||||
# get org_id from activity
|
||||
activity = await activity.find_one(
|
||||
{"activity_id": activity_id}, {"_id": 0}
|
||||
)
|
||||
org_id = activity["org_id"]
|
||||
|
||||
# get block id
|
||||
block_id = str(f"block_{uuid4()}")
|
||||
|
||||
coursechapter_id = activity["coursechapter_id"]
|
||||
|
||||
# get course_id from coursechapter
|
||||
course = await courses.find_one(
|
||||
{"chapters": coursechapter_id},
|
||||
{"_id": 0},
|
||||
)
|
||||
|
||||
block_data = await upload_file_and_return_file_object(
|
||||
request,
|
||||
video_file,
|
||||
activity_id,
|
||||
block_id,
|
||||
["mp4", "webm", "ogg"],
|
||||
block_type,
|
||||
org_id,
|
||||
course["course_id"],
|
||||
)
|
||||
|
||||
# create block
|
||||
block = Block(
|
||||
block_id=block_id,
|
||||
activity_id=activity_id,
|
||||
block_type=block_type,
|
||||
block_data=block_data,
|
||||
org_id=org_id,
|
||||
course_id=course["course_id"],
|
||||
)
|
||||
|
||||
# insert block
|
||||
await blocks.insert_one(block.dict())
|
||||
|
||||
return block
|
||||
|
||||
|
||||
async def get_video_block(request: Request, file_id: str, current_user: PublicUser):
|
||||
blocks = request.app.db["blocks"]
|
||||
|
||||
video_block = await blocks.find_one({"block_id": file_id})
|
||||
|
||||
if video_block:
|
||||
return Block(**video_block)
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Video file does not exist"
|
||||
)
|
||||
12
apps/api/src/services/blocks/schemas/blocks.py
Normal file
12
apps/api/src/services/blocks/schemas/blocks.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
|
||||
from typing import Any, Literal
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Block(BaseModel):
|
||||
block_id: str
|
||||
activity_id: str
|
||||
course_id: str
|
||||
org_id: str
|
||||
block_type: Literal["quizBlock", "videoBlock", "pdfBlock", "imageBlock"]
|
||||
block_data: Any
|
||||
10
apps/api/src/services/blocks/schemas/files.py
Normal file
10
apps/api/src/services/blocks/schemas/files.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class BlockFile(BaseModel):
|
||||
file_id: str
|
||||
file_format: str
|
||||
file_name: str
|
||||
file_size: int
|
||||
file_type: str
|
||||
activity_id: str
|
||||
58
apps/api/src/services/blocks/utils/upload_files.py
Normal file
58
apps/api/src/services/blocks/utils/upload_files.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import uuid
|
||||
from fastapi import HTTPException, Request, UploadFile, status
|
||||
from src.services.blocks.schemas.files import BlockFile
|
||||
from src.services.utils.upload_content import upload_content
|
||||
|
||||
|
||||
async def upload_file_and_return_file_object(
|
||||
request: Request,
|
||||
file: UploadFile,
|
||||
activity_id: str,
|
||||
block_id: str,
|
||||
list_of_allowed_file_formats: list,
|
||||
type_of_block: str,
|
||||
org_id: str,
|
||||
course_id: str,
|
||||
):
|
||||
# get file id
|
||||
file_id = str(uuid.uuid4())
|
||||
|
||||
# get file format
|
||||
file_format = file.filename.split(".")[-1]
|
||||
|
||||
# validate file format
|
||||
if file_format not in list_of_allowed_file_formats:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="File format not supported"
|
||||
)
|
||||
|
||||
# create file
|
||||
file_binary = await file.read()
|
||||
|
||||
# get file size
|
||||
file_size = len(await file.read())
|
||||
|
||||
# get file type
|
||||
file_type = file.content_type
|
||||
|
||||
# get file name
|
||||
file_name = file.filename
|
||||
|
||||
# create file object
|
||||
uploadable_file = BlockFile(
|
||||
file_id=file_id,
|
||||
file_format=file_format,
|
||||
file_name=file_name,
|
||||
file_size=file_size,
|
||||
file_type=file_type,
|
||||
activity_id=activity_id,
|
||||
)
|
||||
|
||||
await upload_content(
|
||||
f"courses/{course_id}/activities/{activity_id}/dynamic/blocks/{type_of_block}/{block_id}",
|
||||
org_id=org_id,
|
||||
file_binary=file_binary,
|
||||
file_and_format=f"{file_id}.{file_format}",
|
||||
)
|
||||
|
||||
return uploadable_file
|
||||
250
apps/api/src/services/courses/activities/activities.py
Normal file
250
apps/api/src/services/courses/activities/activities.py
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
from typing import Literal
|
||||
from pydantic import BaseModel
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles,
|
||||
authorization_verify_if_element_is_public,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
from src.services.users.schemas.users import AnonymousUser, PublicUser
|
||||
from fastapi import HTTPException, status, Request
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
|
||||
#### Classes ####################################################
|
||||
|
||||
|
||||
class Activity(BaseModel):
|
||||
name: str
|
||||
type: str
|
||||
content: object
|
||||
|
||||
|
||||
class ActivityInDB(Activity):
|
||||
activity_id: str
|
||||
course_id: str
|
||||
coursechapter_id: str
|
||||
org_id: str
|
||||
creationDate: str
|
||||
updateDate: str
|
||||
|
||||
|
||||
#### Classes ####################################################
|
||||
|
||||
|
||||
####################################################
|
||||
# CRUD
|
||||
####################################################
|
||||
|
||||
|
||||
async def create_activity(
|
||||
request: Request,
|
||||
activity_object: Activity,
|
||||
org_id: str,
|
||||
coursechapter_id: str,
|
||||
current_user: PublicUser,
|
||||
):
|
||||
activities = request.app.db["activities"]
|
||||
courses = request.app.db["courses"]
|
||||
users = request.app.db["users"]
|
||||
|
||||
# get user
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
# generate activity_id
|
||||
activity_id = str(f"activity_{uuid4()}")
|
||||
|
||||
# verify activity rights
|
||||
await authorization_verify_based_on_roles(
|
||||
request,
|
||||
current_user.user_id,
|
||||
"create",
|
||||
user["roles"],
|
||||
activity_id,
|
||||
)
|
||||
|
||||
# get course_id from activity
|
||||
course = await courses.find_one({"chapters": coursechapter_id})
|
||||
|
||||
# create activity
|
||||
activity = ActivityInDB(
|
||||
**activity_object.dict(),
|
||||
creationDate=str(datetime.now()),
|
||||
coursechapter_id=coursechapter_id,
|
||||
updateDate=str(datetime.now()),
|
||||
activity_id=activity_id,
|
||||
org_id=org_id,
|
||||
course_id=course["course_id"],
|
||||
)
|
||||
await activities.insert_one(activity.dict())
|
||||
|
||||
# update chapter
|
||||
await courses.update_one(
|
||||
{"chapters_content.coursechapter_id": coursechapter_id},
|
||||
{"$addToSet": {"chapters_content.$.activities": activity_id}},
|
||||
)
|
||||
|
||||
return activity
|
||||
|
||||
|
||||
async def get_activity(request: Request, activity_id: str, current_user: PublicUser):
|
||||
activities = request.app.db["activities"]
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
activity = await activities.find_one({"activity_id": activity_id})
|
||||
|
||||
# get course_id from activity
|
||||
coursechapter_id = activity["coursechapter_id"]
|
||||
await courses.find_one({"chapters": coursechapter_id})
|
||||
|
||||
# verify course rights
|
||||
await verify_rights(request, activity["course_id"], current_user, "read")
|
||||
|
||||
if not activity:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
|
||||
)
|
||||
|
||||
activity = ActivityInDB(**activity)
|
||||
return activity
|
||||
|
||||
|
||||
async def update_activity(
|
||||
request: Request,
|
||||
activity_object: Activity,
|
||||
activity_id: str,
|
||||
current_user: PublicUser,
|
||||
):
|
||||
activities = request.app.db["activities"]
|
||||
|
||||
activity = await activities.find_one({"activity_id": activity_id})
|
||||
|
||||
# verify course rights
|
||||
await verify_rights(request, activity_id, current_user, "update")
|
||||
|
||||
if activity:
|
||||
creationDate = activity["creationDate"]
|
||||
|
||||
# get today's date
|
||||
datetime_object = datetime.now()
|
||||
|
||||
updated_course = ActivityInDB(
|
||||
activity_id=activity_id,
|
||||
coursechapter_id=activity["coursechapter_id"],
|
||||
creationDate=creationDate,
|
||||
updateDate=str(datetime_object),
|
||||
course_id=activity["course_id"],
|
||||
org_id=activity["org_id"],
|
||||
**activity_object.dict(),
|
||||
)
|
||||
|
||||
await activities.update_one(
|
||||
{"activity_id": activity_id}, {"$set": updated_course.dict()}
|
||||
)
|
||||
|
||||
return ActivityInDB(**updated_course.dict())
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="activity does not exist"
|
||||
)
|
||||
|
||||
|
||||
async def delete_activity(request: Request, activity_id: str, current_user: PublicUser):
|
||||
activities = request.app.db["activities"]
|
||||
|
||||
activity = await activities.find_one({"activity_id": activity_id})
|
||||
|
||||
# verify course rights
|
||||
await verify_rights(request, activity_id, current_user, "delete")
|
||||
|
||||
if not activity:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="activity does not exist"
|
||||
)
|
||||
|
||||
# Remove Activity
|
||||
isDeleted = await activities.delete_one({"activity_id": activity_id})
|
||||
|
||||
# Remove Activity from chapter
|
||||
courses = request.app.db["courses"]
|
||||
isDeletedFromChapter = await courses.update_one(
|
||||
{"chapters_content.activities": activity_id},
|
||||
{"$pull": {"chapters_content.$.activities": activity_id}},
|
||||
)
|
||||
|
||||
if isDeleted and isDeletedFromChapter:
|
||||
return {"detail": "Activity deleted"}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Unavailable database",
|
||||
)
|
||||
|
||||
|
||||
####################################################
|
||||
# Misc
|
||||
####################################################
|
||||
|
||||
|
||||
async def get_activities(
|
||||
request: Request, coursechapter_id: str, current_user: PublicUser
|
||||
):
|
||||
activities = request.app.db["activities"]
|
||||
|
||||
activities = activities.find({"coursechapter_id": coursechapter_id})
|
||||
|
||||
if not activities:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
|
||||
)
|
||||
|
||||
activities = [
|
||||
ActivityInDB(**activity) for activity in await activities.to_list(length=100)
|
||||
]
|
||||
|
||||
return activities
|
||||
|
||||
|
||||
#### Security ####################################################
|
||||
|
||||
|
||||
async def verify_rights(
|
||||
request: Request,
|
||||
activity_id: str, # course_id in case of read
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
):
|
||||
if action == "read":
|
||||
if current_user.user_id == "anonymous":
|
||||
await authorization_verify_if_element_is_public(
|
||||
request, activity_id, current_user.user_id, action
|
||||
)
|
||||
else:
|
||||
users = request.app.db["users"]
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
await authorization_verify_if_user_is_anon(current_user.user_id)
|
||||
|
||||
await authorization_verify_based_on_roles(
|
||||
request,
|
||||
current_user.user_id,
|
||||
action,
|
||||
user["roles"],
|
||||
activity_id,
|
||||
)
|
||||
else:
|
||||
users = request.app.db["users"]
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
await authorization_verify_if_user_is_anon(current_user.user_id)
|
||||
|
||||
await authorization_verify_based_on_roles(
|
||||
request,
|
||||
current_user.user_id,
|
||||
action,
|
||||
user["roles"],
|
||||
activity_id,
|
||||
)
|
||||
|
||||
|
||||
#### Security ####################################################
|
||||
95
apps/api/src/services/courses/activities/pdf.py
Normal file
95
apps/api/src/services/courses/activities/pdf.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
from src.security.rbac.rbac import authorization_verify_based_on_roles
|
||||
from src.services.courses.activities.uploads.pdfs import upload_pdf
|
||||
from src.services.users.users import PublicUser
|
||||
from src.services.courses.activities.activities import ActivityInDB
|
||||
from fastapi import HTTPException, status, UploadFile, Request
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
async def create_documentpdf_activity(
|
||||
request: Request,
|
||||
name: str,
|
||||
coursechapter_id: str,
|
||||
current_user: PublicUser,
|
||||
pdf_file: UploadFile | None = None,
|
||||
):
|
||||
activities = request.app.db["activities"]
|
||||
courses = request.app.db["courses"]
|
||||
users = request.app.db["users"]
|
||||
|
||||
# get user
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
# generate activity_id
|
||||
activity_id = str(f"activity_{uuid4()}")
|
||||
|
||||
# get org_id from course
|
||||
coursechapter = await courses.find_one(
|
||||
{"chapters_content.coursechapter_id": coursechapter_id}
|
||||
)
|
||||
|
||||
org_id = coursechapter["org_id"]
|
||||
|
||||
# check if pdf_file is not None
|
||||
if not pdf_file:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Pdf : No pdf file provided"
|
||||
)
|
||||
|
||||
if pdf_file.content_type not in ["application/pdf"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Pdf : Wrong pdf format"
|
||||
)
|
||||
|
||||
# get pdf format
|
||||
if pdf_file.filename:
|
||||
pdf_format = pdf_file.filename.split(".")[-1]
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Pdf : No pdf file provided"
|
||||
)
|
||||
|
||||
activity_object = ActivityInDB(
|
||||
org_id=org_id,
|
||||
activity_id=activity_id,
|
||||
coursechapter_id=coursechapter_id,
|
||||
name=name,
|
||||
type="documentpdf",
|
||||
course_id=coursechapter["course_id"],
|
||||
content={
|
||||
"documentpdf": {
|
||||
"filename": "documentpdf." + pdf_format,
|
||||
"activity_id": activity_id,
|
||||
}
|
||||
},
|
||||
creationDate=str(datetime.now()),
|
||||
updateDate=str(datetime.now()),
|
||||
)
|
||||
|
||||
await authorization_verify_based_on_roles(
|
||||
request,
|
||||
current_user.user_id,
|
||||
"create",
|
||||
user["roles"],
|
||||
activity_id,
|
||||
)
|
||||
|
||||
# create activity
|
||||
activity = ActivityInDB(**activity_object.dict())
|
||||
await activities.insert_one(activity.dict())
|
||||
|
||||
# upload pdf
|
||||
if pdf_file:
|
||||
# get pdffile format
|
||||
await upload_pdf(pdf_file, activity_id, org_id, coursechapter["course_id"])
|
||||
|
||||
# todo : choose whether to update the chapter or not
|
||||
# update chapter
|
||||
await courses.update_one(
|
||||
{"chapters_content.coursechapter_id": coursechapter_id},
|
||||
{"$addToSet": {"chapters_content.$.activities": activity_id}},
|
||||
)
|
||||
|
||||
return activity
|
||||
18
apps/api/src/services/courses/activities/uploads/pdfs.py
Normal file
18
apps/api/src/services/courses/activities/uploads/pdfs.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
|
||||
from src.services.utils.upload_content import upload_content
|
||||
|
||||
|
||||
async def upload_pdf(pdf_file, activity_id, org_id, course_id):
|
||||
contents = pdf_file.file.read()
|
||||
pdf_format = pdf_file.filename.split(".")[-1]
|
||||
|
||||
try:
|
||||
await upload_content(
|
||||
f"courses/{course_id}/activities/{activity_id}/documentpdf",
|
||||
org_id,
|
||||
contents,
|
||||
f"documentpdf.{pdf_format}",
|
||||
)
|
||||
|
||||
except Exception:
|
||||
return {"message": "There was an error uploading the file"}
|
||||
18
apps/api/src/services/courses/activities/uploads/videos.py
Normal file
18
apps/api/src/services/courses/activities/uploads/videos.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
|
||||
from src.services.utils.upload_content import upload_content
|
||||
|
||||
|
||||
async def upload_video(video_file, activity_id, org_id, course_id):
|
||||
contents = video_file.file.read()
|
||||
video_format = video_file.filename.split(".")[-1]
|
||||
|
||||
try:
|
||||
await upload_content(
|
||||
f"courses/{course_id}/activities/{activity_id}/video",
|
||||
org_id,
|
||||
contents,
|
||||
f"video.{video_format}",
|
||||
)
|
||||
|
||||
except Exception:
|
||||
return {"message": "There was an error uploading the file"}
|
||||
187
apps/api/src/services/courses/activities/video.py
Normal file
187
apps/api/src/services/courses/activities/video.py
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles,
|
||||
)
|
||||
from src.services.courses.activities.uploads.videos import upload_video
|
||||
from src.services.users.users import PublicUser
|
||||
from src.services.courses.activities.activities import ActivityInDB
|
||||
from fastapi import HTTPException, status, UploadFile, Request
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
async def create_video_activity(
|
||||
request: Request,
|
||||
name: str,
|
||||
coursechapter_id: str,
|
||||
current_user: PublicUser,
|
||||
video_file: UploadFile | None = None,
|
||||
):
|
||||
activities = request.app.db["activities"]
|
||||
courses = request.app.db["courses"]
|
||||
users = request.app.db["users"]
|
||||
|
||||
# get user
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
# generate activity_id
|
||||
activity_id = str(f"activity_{uuid4()}")
|
||||
|
||||
# get org_id from course
|
||||
coursechapter = await courses.find_one(
|
||||
{"chapters_content.coursechapter_id": coursechapter_id}
|
||||
)
|
||||
|
||||
if not coursechapter:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="CourseChapter : No coursechapter found",
|
||||
)
|
||||
|
||||
org_id = coursechapter["org_id"]
|
||||
|
||||
# check if video_file is not None
|
||||
if not video_file:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Video : No video file provided",
|
||||
)
|
||||
|
||||
if video_file.content_type not in ["video/mp4", "video/webm"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Video : Wrong video format"
|
||||
)
|
||||
|
||||
# get video format
|
||||
if video_file.filename:
|
||||
video_format = video_file.filename.split(".")[-1]
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Video : No video file provided",
|
||||
)
|
||||
|
||||
activity_object = ActivityInDB(
|
||||
org_id=org_id,
|
||||
activity_id=activity_id,
|
||||
coursechapter_id=coursechapter_id,
|
||||
course_id=coursechapter["course_id"],
|
||||
name=name,
|
||||
type="video",
|
||||
content={
|
||||
"video": {
|
||||
"filename": "video." + video_format,
|
||||
"activity_id": activity_id,
|
||||
}
|
||||
},
|
||||
creationDate=str(datetime.now()),
|
||||
updateDate=str(datetime.now()),
|
||||
)
|
||||
|
||||
await authorization_verify_based_on_roles(
|
||||
request,
|
||||
current_user.user_id,
|
||||
"create",
|
||||
user["roles"],
|
||||
activity_id,
|
||||
)
|
||||
|
||||
# create activity
|
||||
activity = ActivityInDB(**activity_object.dict())
|
||||
await activities.insert_one(activity.dict())
|
||||
|
||||
# upload video
|
||||
if video_file:
|
||||
# get videofile format
|
||||
await upload_video(video_file, activity_id, org_id, coursechapter["course_id"])
|
||||
|
||||
# todo : choose whether to update the chapter or not
|
||||
# update chapter
|
||||
await courses.update_one(
|
||||
{"chapters_content.coursechapter_id": coursechapter_id},
|
||||
{"$addToSet": {"chapters_content.$.activities": activity_id}},
|
||||
)
|
||||
|
||||
return activity
|
||||
|
||||
|
||||
class ExternalVideo(BaseModel):
|
||||
name: str
|
||||
uri: str
|
||||
type: Literal["youtube", "vimeo"]
|
||||
coursechapter_id: str
|
||||
|
||||
|
||||
class ExternalVideoInDB(BaseModel):
|
||||
activity_id: str
|
||||
|
||||
|
||||
async def create_external_video_activity(
|
||||
request: Request,
|
||||
current_user: PublicUser,
|
||||
data: ExternalVideo,
|
||||
):
|
||||
activities = request.app.db["activities"]
|
||||
courses = request.app.db["courses"]
|
||||
users = request.app.db["users"]
|
||||
|
||||
# get user
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
# generate activity_id
|
||||
activity_id = str(f"activity_{uuid4()}")
|
||||
|
||||
# get org_id from course
|
||||
coursechapter = await courses.find_one(
|
||||
{"chapters_content.coursechapter_id": data.coursechapter_id}
|
||||
)
|
||||
|
||||
if not coursechapter:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="CourseChapter : No coursechapter found",
|
||||
)
|
||||
|
||||
org_id = coursechapter["org_id"]
|
||||
|
||||
activity_object = ActivityInDB(
|
||||
org_id=org_id,
|
||||
activity_id=activity_id,
|
||||
coursechapter_id=data.coursechapter_id,
|
||||
name=data.name,
|
||||
type="video",
|
||||
content={
|
||||
"external_video": {
|
||||
"uri": data.uri,
|
||||
"activity_id": activity_id,
|
||||
"type": data.type,
|
||||
}
|
||||
},
|
||||
course_id=coursechapter["course_id"],
|
||||
creationDate=str(datetime.now()),
|
||||
updateDate=str(datetime.now()),
|
||||
)
|
||||
|
||||
await authorization_verify_based_on_roles(
|
||||
request,
|
||||
current_user.user_id,
|
||||
"create",
|
||||
user["roles"],
|
||||
activity_id,
|
||||
)
|
||||
|
||||
# create activity
|
||||
activity = ActivityInDB(**activity_object.dict())
|
||||
await activities.insert_one(activity.dict())
|
||||
|
||||
# todo : choose whether to update the chapter or not
|
||||
# update chapter
|
||||
await courses.update_one(
|
||||
{"chapters_content.coursechapter_id": data.coursechapter_id},
|
||||
{"$addToSet": {"chapters_content.$.activities": activity_id}},
|
||||
)
|
||||
|
||||
return activity
|
||||
367
apps/api/src/services/courses/chapters.py
Normal file
367
apps/api/src/services/courses/chapters.py
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
from datetime import datetime
|
||||
from typing import List, Literal
|
||||
from uuid import uuid4
|
||||
from pydantic import BaseModel
|
||||
from src.security.auth import non_public_endpoint
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles,
|
||||
authorization_verify_based_on_roles_and_authorship,
|
||||
authorization_verify_if_element_is_public,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
from src.services.courses.courses import Course
|
||||
from src.services.courses.activities.activities import ActivityInDB
|
||||
from src.services.users.users import PublicUser
|
||||
from fastapi import HTTPException, status, Request
|
||||
|
||||
|
||||
class CourseChapter(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
activities: list
|
||||
|
||||
|
||||
class CourseChapterInDB(CourseChapter):
|
||||
coursechapter_id: str
|
||||
course_id: str
|
||||
creationDate: str
|
||||
updateDate: str
|
||||
|
||||
|
||||
# Frontend
|
||||
class CourseChapterMetaData(BaseModel):
|
||||
chapterOrder: List[str]
|
||||
chapters: dict
|
||||
activities: object
|
||||
|
||||
|
||||
#### Classes ####################################################
|
||||
|
||||
####################################################
|
||||
# CRUD
|
||||
####################################################
|
||||
|
||||
|
||||
async def create_coursechapter(
|
||||
request: Request,
|
||||
coursechapter_object: CourseChapter,
|
||||
course_id: str,
|
||||
current_user: PublicUser,
|
||||
):
|
||||
courses = request.app.db["courses"]
|
||||
users = request.app.db["users"]
|
||||
# get course org_id and verify rights
|
||||
await courses.find_one({"course_id": course_id})
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
# generate coursechapter_id with uuid4
|
||||
coursechapter_id = str(f"coursechapter_{uuid4()}")
|
||||
|
||||
hasRoleRights = await authorization_verify_based_on_roles(
|
||||
request, current_user.user_id, "create", user["roles"], course_id
|
||||
)
|
||||
|
||||
if not hasRoleRights:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Roles : Insufficient rights to perform this action",
|
||||
)
|
||||
|
||||
coursechapter = CourseChapterInDB(
|
||||
coursechapter_id=coursechapter_id,
|
||||
creationDate=str(datetime.now()),
|
||||
updateDate=str(datetime.now()),
|
||||
course_id=course_id,
|
||||
**coursechapter_object.dict(),
|
||||
)
|
||||
|
||||
courses.update_one(
|
||||
{"course_id": course_id},
|
||||
{
|
||||
"$addToSet": {
|
||||
"chapters": coursechapter_id,
|
||||
"chapters_content": coursechapter.dict(),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return coursechapter.dict()
|
||||
|
||||
|
||||
async def get_coursechapter(
|
||||
request: Request, coursechapter_id: str, current_user: PublicUser
|
||||
):
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
coursechapter = await courses.find_one(
|
||||
{"chapters_content.coursechapter_id": coursechapter_id}
|
||||
)
|
||||
|
||||
if coursechapter:
|
||||
# verify course rights
|
||||
await verify_rights(request, coursechapter["course_id"], current_user, "read")
|
||||
coursechapter = CourseChapter(**coursechapter)
|
||||
|
||||
return coursechapter
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="CourseChapter does not exist"
|
||||
)
|
||||
|
||||
|
||||
async def update_coursechapter(
|
||||
request: Request,
|
||||
coursechapter_object: CourseChapter,
|
||||
coursechapter_id: str,
|
||||
current_user: PublicUser,
|
||||
):
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
coursechapter = await courses.find_one(
|
||||
{"chapters_content.coursechapter_id": coursechapter_id}
|
||||
)
|
||||
|
||||
if coursechapter:
|
||||
# verify course rights
|
||||
await verify_rights(request, coursechapter["course_id"], current_user, "update")
|
||||
|
||||
coursechapter = CourseChapterInDB(
|
||||
coursechapter_id=coursechapter_id,
|
||||
creationDate=str(datetime.now()),
|
||||
updateDate=str(datetime.now()),
|
||||
course_id=coursechapter["course_id"],
|
||||
**coursechapter_object.dict(),
|
||||
)
|
||||
|
||||
courses.update_one(
|
||||
{"chapters_content.coursechapter_id": coursechapter_id},
|
||||
{"$set": {"chapters_content.$": coursechapter.dict()}},
|
||||
)
|
||||
|
||||
return coursechapter
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Coursechapter does not exist"
|
||||
)
|
||||
|
||||
|
||||
async def delete_coursechapter(
|
||||
request: Request, coursechapter_id: str, current_user: PublicUser
|
||||
):
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
course = await courses.find_one(
|
||||
{"chapters_content.coursechapter_id": coursechapter_id}
|
||||
)
|
||||
|
||||
if course:
|
||||
# verify course rights
|
||||
await verify_rights(request, course["course_id"], current_user, "delete")
|
||||
|
||||
# Remove coursechapter from course
|
||||
await courses.update_one(
|
||||
{"course_id": course["course_id"]},
|
||||
{"$pull": {"chapters": coursechapter_id}},
|
||||
)
|
||||
|
||||
await courses.update_one(
|
||||
{"chapters_content.coursechapter_id": coursechapter_id},
|
||||
{"$pull": {"chapters_content": {"coursechapter_id": coursechapter_id}}},
|
||||
)
|
||||
|
||||
return {"message": "Coursechapter deleted"}
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
|
||||
)
|
||||
|
||||
|
||||
####################################################
|
||||
# Misc
|
||||
####################################################
|
||||
|
||||
|
||||
async def get_coursechapters(
|
||||
request: Request, course_id: str, page: int = 1, limit: int = 10
|
||||
):
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
course = await courses.find_one({"course_id": course_id})
|
||||
|
||||
if course:
|
||||
course = Course(**course)
|
||||
coursechapters = course.chapters_content
|
||||
|
||||
return coursechapters
|
||||
|
||||
|
||||
async def get_coursechapters_meta(
|
||||
request: Request, course_id: str, current_user: PublicUser
|
||||
):
|
||||
courses = request.app.db["courses"]
|
||||
activities = request.app.db["activities"]
|
||||
|
||||
await non_public_endpoint(current_user)
|
||||
|
||||
await verify_rights(request, course_id, current_user, "read")
|
||||
|
||||
coursechapters = await courses.find_one(
|
||||
{"course_id": course_id}, {"chapters": 1, "chapters_content": 1, "_id": 0}
|
||||
)
|
||||
|
||||
coursechapters = coursechapters
|
||||
|
||||
if not coursechapters:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
|
||||
)
|
||||
|
||||
# activities
|
||||
coursechapter_activityIds_global = []
|
||||
|
||||
# chapters
|
||||
chapters = {}
|
||||
if coursechapters["chapters_content"]:
|
||||
for coursechapter in coursechapters["chapters_content"]:
|
||||
coursechapter = CourseChapterInDB(**coursechapter)
|
||||
coursechapter_activityIds = []
|
||||
|
||||
for activity in coursechapter.activities:
|
||||
coursechapter_activityIds.append(activity)
|
||||
coursechapter_activityIds_global.append(activity)
|
||||
|
||||
chapters[coursechapter.coursechapter_id] = {
|
||||
"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):
|
||||
activity = ActivityInDB(**activity)
|
||||
activities_list[activity.activity_id] = {
|
||||
"id": activity.activity_id,
|
||||
"name": activity.name,
|
||||
"type": activity.type,
|
||||
"content": activity.content,
|
||||
}
|
||||
|
||||
final = {
|
||||
"chapters": chapters,
|
||||
"chapterOrder": coursechapters["chapters"],
|
||||
"activities": activities_list,
|
||||
}
|
||||
|
||||
return final
|
||||
|
||||
|
||||
async def update_coursechapters_meta(
|
||||
request: Request,
|
||||
course_id: str,
|
||||
coursechapters_metadata: CourseChapterMetaData,
|
||||
current_user: PublicUser,
|
||||
):
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
await verify_rights(request, course_id, current_user, "update")
|
||||
|
||||
# update chapters in course
|
||||
await courses.update_one(
|
||||
{"course_id": course_id},
|
||||
{"$set": {"chapters": coursechapters_metadata.chapterOrder}},
|
||||
)
|
||||
|
||||
if coursechapters_metadata.chapters is not None:
|
||||
for (
|
||||
coursechapter_id,
|
||||
chapter_metadata,
|
||||
) in coursechapters_metadata.chapters.items():
|
||||
filter_query = {"chapters_content.coursechapter_id": coursechapter_id}
|
||||
update_query = {
|
||||
"$set": {
|
||||
"chapters_content.$.activities": chapter_metadata["activityIds"]
|
||||
}
|
||||
}
|
||||
result = await courses.update_one(filter_query, update_query)
|
||||
if result.matched_count == 0:
|
||||
# handle error when no documents are matched by the filter query
|
||||
print(f"No documents found for course chapter ID {coursechapter_id}")
|
||||
|
||||
# update activities in coursechapters
|
||||
activity = request.app.db["activities"]
|
||||
if coursechapters_metadata.chapters is not None:
|
||||
for (
|
||||
coursechapter_id,
|
||||
chapter_metadata,
|
||||
) in coursechapters_metadata.chapters.items():
|
||||
# Update coursechapter_id in activities
|
||||
filter_query = {"activity_id": {"$in": chapter_metadata["activityIds"]}}
|
||||
update_query = {"$set": {"coursechapter_id": coursechapter_id}}
|
||||
|
||||
result = await activity.update_many(filter_query, update_query)
|
||||
if result.matched_count == 0:
|
||||
# handle error when no documents are matched by the filter query
|
||||
print(f"No documents found for course chapter ID {coursechapter_id}")
|
||||
|
||||
return {"detail": "coursechapters metadata updated"}
|
||||
|
||||
|
||||
#### Security ####################################################
|
||||
|
||||
|
||||
async def verify_rights(
|
||||
request: Request,
|
||||
course_id: str,
|
||||
current_user: PublicUser,
|
||||
action: Literal["read", "update", "delete"],
|
||||
):
|
||||
courses = request.app.db["courses"]
|
||||
users = request.app.db["users"]
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
course = await courses.find_one({"course_id": course_id})
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
|
||||
)
|
||||
|
||||
if action == "read":
|
||||
if current_user.user_id == "anonymous":
|
||||
await authorization_verify_if_element_is_public(
|
||||
request, course_id, current_user.user_id, action
|
||||
)
|
||||
else:
|
||||
users = request.app.db["users"]
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
await authorization_verify_if_user_is_anon(current_user.user_id)
|
||||
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request,
|
||||
current_user.user_id,
|
||||
action,
|
||||
user["roles"],
|
||||
course_id,
|
||||
)
|
||||
else:
|
||||
users = request.app.db["users"]
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
await authorization_verify_if_user_is_anon(current_user.user_id)
|
||||
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request,
|
||||
current_user.user_id,
|
||||
action,
|
||||
user["roles"],
|
||||
course_id,
|
||||
)
|
||||
|
||||
|
||||
#### Security ####################################################
|
||||
242
apps/api/src/services/courses/collections.py
Normal file
242
apps/api/src/services/courses/collections.py
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
from typing import List, Literal
|
||||
from uuid import uuid4
|
||||
from pydantic import BaseModel
|
||||
from src.security.rbac.rbac import authorization_verify_based_on_roles_and_authorship, authorization_verify_if_user_is_anon
|
||||
from src.services.users.users import PublicUser
|
||||
from fastapi import HTTPException, status, Request
|
||||
|
||||
#### Classes ####################################################
|
||||
|
||||
|
||||
class Collection(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
courses: List[str] # course_id
|
||||
public: bool
|
||||
org_id: str # org_id
|
||||
|
||||
|
||||
class CollectionInDB(Collection):
|
||||
collection_id: str
|
||||
authors: List[str] # user_id
|
||||
|
||||
|
||||
#### Classes ####################################################
|
||||
|
||||
####################################################
|
||||
# CRUD
|
||||
####################################################
|
||||
|
||||
|
||||
async def get_collection(
|
||||
request: Request, collection_id: str, current_user: PublicUser
|
||||
):
|
||||
collections = request.app.db["collections"]
|
||||
|
||||
collection = await collections.find_one({"collection_id": collection_id})
|
||||
|
||||
# verify collection rights
|
||||
await verify_collection_rights(
|
||||
request, collection_id, current_user, "read", collection["org_id"]
|
||||
)
|
||||
|
||||
if not collection:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
|
||||
)
|
||||
|
||||
collection = Collection(**collection)
|
||||
|
||||
# add courses to collection
|
||||
courses = request.app.db["courses"]
|
||||
courseids = [course for course in collection.courses]
|
||||
|
||||
collection.courses = []
|
||||
collection.courses = courses.find({"course_id": {"$in": courseids}}, {"_id": 0})
|
||||
|
||||
collection.courses = [
|
||||
course for course in await collection.courses.to_list(length=100)
|
||||
]
|
||||
|
||||
return collection
|
||||
|
||||
|
||||
async def create_collection(
|
||||
request: Request, collection_object: Collection, current_user: PublicUser
|
||||
):
|
||||
collections = request.app.db["collections"]
|
||||
|
||||
# find if collection already exists using name
|
||||
isCollectionNameAvailable = await collections.find_one(
|
||||
{"name": collection_object.name}
|
||||
)
|
||||
|
||||
# TODO
|
||||
# await verify_collection_rights("*", current_user, "create")
|
||||
|
||||
if isCollectionNameAvailable:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Collection name already exists",
|
||||
)
|
||||
|
||||
# generate collection_id with uuid4
|
||||
collection_id = str(f"collection_{uuid4()}")
|
||||
|
||||
collection = CollectionInDB(
|
||||
collection_id=collection_id,
|
||||
authors=[current_user.user_id],
|
||||
**collection_object.dict(),
|
||||
)
|
||||
|
||||
collection_in_db = await collections.insert_one(collection.dict())
|
||||
|
||||
if not collection_in_db:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Unavailable database",
|
||||
)
|
||||
|
||||
return collection.dict()
|
||||
|
||||
|
||||
async def update_collection(
|
||||
request: Request,
|
||||
collection_object: Collection,
|
||||
collection_id: str,
|
||||
current_user: PublicUser,
|
||||
):
|
||||
# verify collection rights
|
||||
|
||||
collections = request.app.db["collections"]
|
||||
|
||||
collection = await collections.find_one({"collection_id": collection_id})
|
||||
|
||||
await verify_collection_rights(
|
||||
request, collection_id, current_user, "update", collection["org_id"]
|
||||
)
|
||||
|
||||
if not collection:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
|
||||
)
|
||||
|
||||
updated_collection = CollectionInDB(
|
||||
collection_id=collection_id, **collection_object.dict()
|
||||
)
|
||||
|
||||
await collections.update_one(
|
||||
{"collection_id": collection_id}, {"$set": updated_collection.dict()}
|
||||
)
|
||||
|
||||
return Collection(**updated_collection.dict())
|
||||
|
||||
|
||||
async def delete_collection(
|
||||
request: Request, collection_id: str, current_user: PublicUser
|
||||
):
|
||||
collections = request.app.db["collections"]
|
||||
|
||||
collection = await collections.find_one({"collection_id": collection_id})
|
||||
|
||||
await verify_collection_rights(
|
||||
request, collection_id, current_user, "delete", collection["org_id"]
|
||||
)
|
||||
|
||||
if not collection:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
|
||||
)
|
||||
|
||||
isDeleted = await collections.delete_one({"collection_id": collection_id})
|
||||
|
||||
if isDeleted:
|
||||
return {"detail": "collection deleted"}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Unavailable database",
|
||||
)
|
||||
|
||||
|
||||
####################################################
|
||||
# Misc
|
||||
####################################################
|
||||
|
||||
|
||||
async def get_collections(
|
||||
request: Request,
|
||||
org_id: str,
|
||||
current_user: PublicUser,
|
||||
page: int = 1,
|
||||
limit: int = 10,
|
||||
):
|
||||
collections = request.app.db["collections"]
|
||||
|
||||
|
||||
if current_user.user_id == "anonymous":
|
||||
all_collections = collections.find(
|
||||
{"org_id": org_id, "public": True}, {"_id": 0}
|
||||
)
|
||||
else:
|
||||
# get all collections from database without ObjectId
|
||||
all_collections = (
|
||||
collections.find({"org_id": org_id})
|
||||
.sort("name", 1)
|
||||
.skip(10 * (page - 1))
|
||||
.limit(limit)
|
||||
)
|
||||
|
||||
# create list of collections and include courses in each collection
|
||||
collections_list = []
|
||||
for collection in await all_collections.to_list(length=100):
|
||||
collection = CollectionInDB(**collection)
|
||||
collections_list.append(collection)
|
||||
|
||||
collection_courses = [course for course in collection.courses]
|
||||
# add courses to collection
|
||||
courses = request.app.db["courses"]
|
||||
collection.courses = []
|
||||
collection.courses = courses.find(
|
||||
{"course_id": {"$in": collection_courses}}, {"_id": 0}
|
||||
)
|
||||
|
||||
collection.courses = [
|
||||
course for course in await collection.courses.to_list(length=100)
|
||||
]
|
||||
|
||||
return collections_list
|
||||
|
||||
|
||||
#### Security ####################################################
|
||||
|
||||
|
||||
async def verify_collection_rights(
|
||||
request: Request,
|
||||
collection_id: str,
|
||||
current_user: PublicUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
org_id: str,
|
||||
):
|
||||
collections = request.app.db["collections"]
|
||||
users = request.app.db["users"]
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
collection = await collections.find_one({"collection_id": collection_id})
|
||||
|
||||
if not collection and action != "create" and collection_id != "*":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
|
||||
)
|
||||
|
||||
# Collections are public by default for now
|
||||
if current_user.user_id == "anonymous" and action == "read":
|
||||
return True
|
||||
|
||||
await authorization_verify_if_user_is_anon(current_user.user_id)
|
||||
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.user_id, action, user["roles"], collection_id
|
||||
)
|
||||
|
||||
|
||||
#### Security ####################################################
|
||||
413
apps/api/src/services/courses/courses.py
Normal file
413
apps/api/src/services/courses/courses.py
Normal file
|
|
@ -0,0 +1,413 @@
|
|||
import json
|
||||
from typing import List, Literal, Optional
|
||||
from uuid import uuid4
|
||||
from pydantic import BaseModel
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles,
|
||||
authorization_verify_based_on_roles_and_authorship,
|
||||
authorization_verify_if_element_is_public,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
from src.services.courses.activities.activities import ActivityInDB
|
||||
from src.services.courses.thumbnails import upload_thumbnail
|
||||
from src.services.users.schemas.users import AnonymousUser
|
||||
from src.services.users.users import PublicUser
|
||||
from fastapi import HTTPException, Request, status, UploadFile
|
||||
from datetime import datetime
|
||||
|
||||
#### Classes ####################################################
|
||||
|
||||
|
||||
class Course(BaseModel):
|
||||
name: str
|
||||
mini_description: str
|
||||
description: str
|
||||
learnings: List[str]
|
||||
thumbnail: str
|
||||
public: bool
|
||||
chapters: List[str]
|
||||
chapters_content: Optional[List]
|
||||
org_id: str
|
||||
|
||||
|
||||
class CourseInDB(Course):
|
||||
course_id: str
|
||||
creationDate: str
|
||||
updateDate: str
|
||||
authors: List[str]
|
||||
|
||||
|
||||
# TODO : wow terrible, fix this
|
||||
# those models need to be available only in the chapters service
|
||||
class CourseChapter(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
activities: list
|
||||
|
||||
|
||||
class CourseChapterInDB(CourseChapter):
|
||||
coursechapter_id: str
|
||||
course_id: str
|
||||
creationDate: str
|
||||
updateDate: str
|
||||
|
||||
|
||||
#### Classes ####################################################
|
||||
|
||||
# TODO : Add courses photo & cover upload and delete
|
||||
|
||||
|
||||
####################################################
|
||||
# CRUD
|
||||
####################################################
|
||||
|
||||
|
||||
async def get_course(request: Request, course_id: str, current_user: PublicUser):
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
course = await courses.find_one({"course_id": course_id})
|
||||
|
||||
# verify course rights
|
||||
await verify_rights(request, course_id, current_user, "read")
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
|
||||
)
|
||||
|
||||
course = Course(**course)
|
||||
return course
|
||||
|
||||
|
||||
async def get_course_meta(request: Request, course_id: str, current_user: PublicUser):
|
||||
courses = request.app.db["courses"]
|
||||
trails = request.app.db["trails"]
|
||||
|
||||
course = await courses.find_one({"course_id": course_id})
|
||||
activities = request.app.db["activities"]
|
||||
|
||||
# verify course rights
|
||||
await verify_rights(request, course_id, current_user, "read")
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
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}
|
||||
)
|
||||
|
||||
# activities
|
||||
coursechapter_activityIds_global = []
|
||||
|
||||
# chapters
|
||||
chapters = {}
|
||||
if coursechapters["chapters_content"]:
|
||||
for coursechapter in coursechapters["chapters_content"]:
|
||||
coursechapter = CourseChapterInDB(**coursechapter)
|
||||
coursechapter_activityIds = []
|
||||
|
||||
for activity in coursechapter.activities:
|
||||
coursechapter_activityIds.append(activity)
|
||||
coursechapter_activityIds_global.append(activity)
|
||||
|
||||
chapters[coursechapter.coursechapter_id] = {
|
||||
"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):
|
||||
activity = ActivityInDB(**activity)
|
||||
activities_list[activity.activity_id] = {
|
||||
"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"]
|
||||
],
|
||||
}
|
||||
)
|
||||
course = CourseInDB(**course)
|
||||
|
||||
# Get activity by user
|
||||
trail = await trails.find_one(
|
||||
{"courses.course_id": course_id, "user_id": current_user.user_id}
|
||||
)
|
||||
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,
|
||||
)
|
||||
else:
|
||||
trail_course = ""
|
||||
|
||||
return {
|
||||
"course": course,
|
||||
"chapters": chapters_list_with_activities,
|
||||
"trail": trail_course,
|
||||
}
|
||||
|
||||
|
||||
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"]
|
||||
users = request.app.db["users"]
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
# generate course_id with uuid4
|
||||
course_id = str(f"course_{uuid4()}")
|
||||
|
||||
# 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 authorization_verify_based_on_roles(
|
||||
request,
|
||||
current_user.user_id,
|
||||
"create",
|
||||
user["roles"],
|
||||
course_id,
|
||||
)
|
||||
|
||||
|
||||
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.org_id, course_id
|
||||
)
|
||||
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_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",
|
||||
)
|
||||
|
||||
return course.dict()
|
||||
|
||||
|
||||
async def update_course_thumbnail(
|
||||
request: Request,
|
||||
course_id: str,
|
||||
current_user: PublicUser,
|
||||
thumbnail_file: UploadFile | None = None,
|
||||
):
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
course = await courses.find_one({"course_id": course_id})
|
||||
|
||||
# verify course rights
|
||||
await verify_rights(request, course_id, current_user, "update")
|
||||
|
||||
# TODO(fix) : the implementation here is clearly not the best one
|
||||
if course:
|
||||
creationDate = course["creationDate"]
|
||||
authors = course["authors"]
|
||||
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, course.org_id, course_id
|
||||
)
|
||||
|
||||
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()}
|
||||
)
|
||||
|
||||
return CourseInDB(**updated_course.dict())
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
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
|
||||
):
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
course = await courses.find_one({"course_id": course_id})
|
||||
|
||||
# verify course rights
|
||||
await verify_rights(request, course_id, current_user, "update")
|
||||
|
||||
if course:
|
||||
creationDate = course["creationDate"]
|
||||
authors = course["authors"]
|
||||
|
||||
# get today's date
|
||||
datetime_object = datetime.now()
|
||||
|
||||
updated_course = CourseInDB(
|
||||
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()}
|
||||
)
|
||||
|
||||
return CourseInDB(**updated_course.dict())
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
|
||||
)
|
||||
|
||||
|
||||
async def delete_course(request: Request, course_id: str, current_user: PublicUser):
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
course = await courses.find_one({"course_id": course_id})
|
||||
|
||||
# verify course rights
|
||||
await verify_rights(request, course_id, current_user, "delete")
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
|
||||
)
|
||||
|
||||
isDeleted = await courses.delete_one({"course_id": course_id})
|
||||
|
||||
if isDeleted:
|
||||
return {"detail": "Course deleted"}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Unavailable database",
|
||||
)
|
||||
|
||||
|
||||
####################################################
|
||||
# Misc
|
||||
####################################################
|
||||
|
||||
|
||||
async def get_courses_orgslug(
|
||||
request: Request,
|
||||
current_user: PublicUser,
|
||||
page: int = 1,
|
||||
limit: int = 10,
|
||||
org_slug: str | None = None,
|
||||
):
|
||||
courses = request.app.db["courses"]
|
||||
orgs = request.app.db["organizations"]
|
||||
|
||||
# get org_id from slug
|
||||
org = await orgs.find_one({"slug": org_slug})
|
||||
|
||||
if not org:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist"
|
||||
)
|
||||
|
||||
# show only public courses if user is not logged in
|
||||
if current_user.user_id == "anonymous":
|
||||
all_courses = (
|
||||
courses.find({"org_id": org["org_id"], "public": True})
|
||||
.sort("name", 1)
|
||||
.skip(10 * (page - 1))
|
||||
.limit(limit)
|
||||
)
|
||||
else:
|
||||
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)
|
||||
]
|
||||
|
||||
|
||||
#### Security ####################################################
|
||||
|
||||
|
||||
async def verify_rights(
|
||||
request: Request,
|
||||
course_id: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
):
|
||||
if action == "read":
|
||||
if current_user.user_id == "anonymous":
|
||||
await authorization_verify_if_element_is_public(
|
||||
request, course_id, current_user.user_id, action
|
||||
)
|
||||
else:
|
||||
users = request.app.db["users"]
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request,
|
||||
current_user.user_id,
|
||||
action,
|
||||
user["roles"],
|
||||
course_id,
|
||||
)
|
||||
else:
|
||||
users = request.app.db["users"]
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
await authorization_verify_if_user_is_anon(current_user.user_id)
|
||||
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request,
|
||||
current_user.user_id,
|
||||
action,
|
||||
user["roles"],
|
||||
course_id,
|
||||
)
|
||||
|
||||
|
||||
#### Security ####################################################
|
||||
16
apps/api/src/services/courses/thumbnails.py
Normal file
16
apps/api/src/services/courses/thumbnails.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
|
||||
from src.services.utils.upload_content import upload_content
|
||||
|
||||
|
||||
async def upload_thumbnail(thumbnail_file, name_in_disk, org_id, course_id):
|
||||
contents = thumbnail_file.file.read()
|
||||
try:
|
||||
await upload_content(
|
||||
f"courses/{course_id}/thumbnails",
|
||||
org_id,
|
||||
contents,
|
||||
f"{name_in_disk}",
|
||||
)
|
||||
|
||||
except Exception:
|
||||
return {"message": "There was an error uploading the file"}
|
||||
0
apps/api/src/services/dev/__init__.py
Normal file
0
apps/api/src/services/dev/__init__.py
Normal file
18
apps/api/src/services/dev/dev.py
Normal file
18
apps/api/src/services/dev/dev.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
from fastapi import HTTPException
|
||||
from config.config import get_learnhouse_config
|
||||
|
||||
|
||||
def isDevModeEnabled():
|
||||
config = get_learnhouse_config()
|
||||
if config.general_config.development_mode:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def isDevModeEnabledOrRaise():
|
||||
config = get_learnhouse_config()
|
||||
if config.general_config.development_mode:
|
||||
return True
|
||||
else:
|
||||
raise HTTPException(status_code=403, detail="Development mode is disabled")
|
||||
0
apps/api/src/services/dev/mocks/__init__.py
Normal file
0
apps/api/src/services/dev/mocks/__init__.py
Normal file
214
apps/api/src/services/dev/mocks/initial.py
Normal file
214
apps/api/src/services/dev/mocks/initial.py
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
import os
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
from fastapi import Request
|
||||
from src.security.security import security_hash_password
|
||||
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.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
|
||||
|
||||
|
||||
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
|
||||
########################################
|
||||
|
||||
database_users = request.app.db["users"]
|
||||
await database_users.delete_many({})
|
||||
|
||||
users = []
|
||||
admin_user = UserInDB(
|
||||
user_id="user_admin",
|
||||
creation_date=str(datetime.now()),
|
||||
update_date=str(datetime.now()),
|
||||
roles= [],
|
||||
orgs=[],
|
||||
username="admin",
|
||||
email="admin@admin.admin",
|
||||
password=str(await security_hash_password("admin")),
|
||||
)
|
||||
|
||||
await database_users.insert_one(admin_user.dict())
|
||||
|
||||
# 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")
|
||||
# Create roles
|
||||
########################################
|
||||
|
||||
database_roles = request.app.db["roles"]
|
||||
await database_roles.delete_many({})
|
||||
|
||||
|
||||
roles = []
|
||||
admin_role = RoleInDB(
|
||||
name="Admin",
|
||||
description="Admin",
|
||||
elements=Elements(
|
||||
courses=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
users=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
houses=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
collections=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
organizations=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
coursechapters=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
activities=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
),
|
||||
org_id="org_test",
|
||||
role_id="role_admin",
|
||||
created_at=str(datetime.now()),
|
||||
updated_at=str(datetime.now()),
|
||||
)
|
||||
|
||||
roles.append(admin_role)
|
||||
|
||||
for role in roles:
|
||||
database_roles.insert_one(role.dict())
|
||||
|
||||
|
||||
# Create organizations
|
||||
########################################
|
||||
|
||||
database_orgs = request.app.db["organizations"]
|
||||
await database_orgs.delete_many({})
|
||||
|
||||
organizations = []
|
||||
for i in range(0, 2):
|
||||
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,
|
||||
logo="",
|
||||
default=False
|
||||
)
|
||||
organizations.append(org)
|
||||
await create_org(request, org, current_user)
|
||||
|
||||
|
||||
# Generate Courses and CourseChapters
|
||||
########################################
|
||||
|
||||
database_courses = request.app.db["courses"]
|
||||
await database_courses.delete_many({})
|
||||
|
||||
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):
|
||||
|
||||
# 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],
|
||||
chapters_content=[],
|
||||
)
|
||||
|
||||
courses = request.app.db["courses"]
|
||||
name_in_disk = f"test_mock{course_id}.jpeg"
|
||||
|
||||
image = requests.get(
|
||||
"https://source.unsplash.com/random/800x600/?img=1")
|
||||
|
||||
# check if folder exists and create it if not
|
||||
if not os.path.exists("content/uploads/img"):
|
||||
|
||||
os.makedirs("content/uploads/img")
|
||||
|
||||
with open(f"content/uploads/img/{name_in_disk}", "wb") as f:
|
||||
f.write(image.content)
|
||||
|
||||
course.thumbnail = name_in_disk
|
||||
|
||||
course = CourseInDB(**course.dict())
|
||||
await courses.insert_one(course.dict())
|
||||
|
||||
# create chapters
|
||||
for i in range(0, 5):
|
||||
coursechapter = CourseChapter(
|
||||
name=fake_multilang.unique.sentence(),
|
||||
description=fake_multilang.unique.text(),
|
||||
activities=[],
|
||||
)
|
||||
coursechapter = await create_coursechapter(request,coursechapter, course_id, current_user)
|
||||
if coursechapter:
|
||||
# create activities
|
||||
for i in range(0, 5):
|
||||
activity = Activity(
|
||||
name=fake_multilang.unique.sentence(),
|
||||
type="dynamic",
|
||||
content={},
|
||||
)
|
||||
activity = await create_activity(request,activity, "org_test", coursechapter['coursechapter_id'], current_user)
|
||||
0
apps/api/src/services/install/__init__.py
Normal file
0
apps/api/src/services/install/__init__.py
Normal file
419
apps/api/src/services/install/install.py
Normal file
419
apps/api/src/services/install/install.py
Normal file
|
|
@ -0,0 +1,419 @@
|
|||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
from fastapi import HTTPException, Request, status
|
||||
from pydantic import BaseModel
|
||||
import requests
|
||||
from config.config import get_learnhouse_config
|
||||
from src.security.security import security_hash_password
|
||||
from src.services.courses.activities.activities import Activity, create_activity
|
||||
from src.services.courses.chapters import create_coursechapter, CourseChapter
|
||||
from src.services.courses.courses import CourseInDB
|
||||
|
||||
from src.services.orgs.schemas.orgs import Organization, OrganizationInDB
|
||||
from faker import Faker
|
||||
|
||||
|
||||
from src.services.roles.schemas.roles import Elements, Permission, RoleInDB
|
||||
from src.services.users.schemas.users import (
|
||||
PublicUser,
|
||||
User,
|
||||
UserInDB,
|
||||
UserOrganization,
|
||||
UserRolesInOrganization,
|
||||
UserWithPassword,
|
||||
)
|
||||
|
||||
|
||||
class InstallInstance(BaseModel):
|
||||
install_id: str
|
||||
created_date: str
|
||||
updated_date: str
|
||||
step: int
|
||||
data: dict
|
||||
|
||||
|
||||
async def isInstallModeEnabled():
|
||||
config = get_learnhouse_config()
|
||||
|
||||
if config.general_config.install_mode:
|
||||
return True
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Install mode is not enabled",
|
||||
)
|
||||
|
||||
|
||||
async def create_install_instance(request: Request, data: dict):
|
||||
installs = request.app.db["installs"]
|
||||
|
||||
# get install_id
|
||||
install_id = str(f"install_{uuid4()}")
|
||||
created_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
updated_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
step = 1
|
||||
|
||||
# create install
|
||||
install = InstallInstance(
|
||||
install_id=install_id,
|
||||
created_date=created_date,
|
||||
updated_date=updated_date,
|
||||
step=step,
|
||||
data=data,
|
||||
)
|
||||
|
||||
# insert install
|
||||
installs.insert_one(install.dict())
|
||||
|
||||
return install
|
||||
|
||||
|
||||
async def get_latest_install_instance(request: Request):
|
||||
installs = request.app.db["installs"]
|
||||
|
||||
# get latest created install instance using find_one
|
||||
install = await installs.find_one(
|
||||
sort=[("created_date", -1)], limit=1, projection={"_id": 0}
|
||||
)
|
||||
|
||||
if install is None:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="No install instance found",
|
||||
)
|
||||
|
||||
else:
|
||||
install = InstallInstance(**install)
|
||||
|
||||
return install
|
||||
|
||||
|
||||
async def update_install_instance(request: Request, data: dict, step: int):
|
||||
installs = request.app.db["installs"]
|
||||
|
||||
# get latest created install
|
||||
install = await installs.find_one(
|
||||
sort=[("created_date", -1)], limit=1, projection={"_id": 0}
|
||||
)
|
||||
|
||||
if install is None:
|
||||
return None
|
||||
|
||||
else:
|
||||
# update install
|
||||
install["data"] = data
|
||||
install["step"] = step
|
||||
install["updated_date"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# update install
|
||||
await installs.update_one(
|
||||
{"install_id": install["install_id"]}, {"$set": install}
|
||||
)
|
||||
|
||||
install = InstallInstance(**install)
|
||||
|
||||
return install
|
||||
|
||||
|
||||
############################################################################################################
|
||||
# Steps
|
||||
############################################################################################################
|
||||
|
||||
|
||||
# Install Default roles
|
||||
async def install_default_elements(request: Request, data: dict):
|
||||
roles = request.app.db["roles"]
|
||||
|
||||
# check if default roles ADMIN_ROLE and USER_ROLE already exist
|
||||
admin_role = await roles.find_one({"role_id": "role_admin"})
|
||||
user_role = await roles.find_one({"role_id": "role_member"})
|
||||
|
||||
if admin_role is not None or user_role is not None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Default roles already exist",
|
||||
)
|
||||
|
||||
# get default roles
|
||||
ADMIN_ROLE = RoleInDB(
|
||||
name="Admin Role",
|
||||
description="This role grants all permissions to the user",
|
||||
elements=Elements(
|
||||
courses=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
users=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
houses=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
collections=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
organizations=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
coursechapters=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
activities=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
),
|
||||
org_id="*",
|
||||
role_id="role_admin",
|
||||
created_at=str(datetime.now()),
|
||||
updated_at=str(datetime.now()),
|
||||
)
|
||||
|
||||
USER_ROLE = RoleInDB(
|
||||
name="Member Role",
|
||||
description="This role grants read-only permissions to the user",
|
||||
elements=Elements(
|
||||
courses=Permission(
|
||||
action_create=False,
|
||||
action_read=True,
|
||||
action_update=False,
|
||||
action_delete=False,
|
||||
),
|
||||
users=Permission(
|
||||
action_create=False,
|
||||
action_read=True,
|
||||
action_update=False,
|
||||
action_delete=False,
|
||||
),
|
||||
houses=Permission(
|
||||
action_create=False,
|
||||
action_read=True,
|
||||
action_update=False,
|
||||
action_delete=False,
|
||||
),
|
||||
collections=Permission(
|
||||
action_create=False,
|
||||
action_read=True,
|
||||
action_update=False,
|
||||
action_delete=False,
|
||||
),
|
||||
organizations=Permission(
|
||||
action_create=False,
|
||||
action_read=True,
|
||||
action_update=False,
|
||||
action_delete=False,
|
||||
),
|
||||
coursechapters=Permission(
|
||||
action_create=False,
|
||||
action_read=True,
|
||||
action_update=False,
|
||||
action_delete=False,
|
||||
),
|
||||
activities=Permission(
|
||||
action_create=False,
|
||||
action_read=True,
|
||||
action_update=False,
|
||||
action_delete=False,
|
||||
),
|
||||
),
|
||||
org_id="*",
|
||||
role_id="role_member",
|
||||
created_at=str(datetime.now()),
|
||||
updated_at=str(datetime.now()),
|
||||
)
|
||||
|
||||
try:
|
||||
# insert default roles
|
||||
await roles.insert_many([USER_ROLE.dict(), ADMIN_ROLE.dict()])
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Error while inserting default roles",
|
||||
)
|
||||
|
||||
|
||||
# Organization creation
|
||||
async def install_create_organization(
|
||||
request: Request,
|
||||
org_object: Organization,
|
||||
):
|
||||
orgs = request.app.db["organizations"]
|
||||
request.app.db["users"]
|
||||
|
||||
# find if org already exists using name
|
||||
|
||||
isOrgAvailable = await orgs.find_one({"slug": org_object.slug.lower()})
|
||||
|
||||
if isOrgAvailable:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Organization slug already exists",
|
||||
)
|
||||
|
||||
# generate org_id with uuid4
|
||||
org_id = str(f"org_{uuid4()}")
|
||||
|
||||
org = OrganizationInDB(org_id=org_id, **org_object.dict())
|
||||
|
||||
org_in_db = await orgs.insert_one(org.dict())
|
||||
|
||||
if not org_in_db:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Unavailable database",
|
||||
)
|
||||
|
||||
return org.dict()
|
||||
|
||||
|
||||
async def install_create_organization_user(
|
||||
request: Request, user_object: UserWithPassword, org_slug: str
|
||||
):
|
||||
users = request.app.db["users"]
|
||||
|
||||
isUsernameAvailable = await users.find_one({"username": user_object.username})
|
||||
isEmailAvailable = await users.find_one({"email": user_object.email})
|
||||
|
||||
if isUsernameAvailable:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Username already exists"
|
||||
)
|
||||
|
||||
if isEmailAvailable:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Email already exists"
|
||||
)
|
||||
|
||||
# Generate user_id with uuid4
|
||||
user_id = str(f"user_{uuid4()}")
|
||||
|
||||
# Set the username & hash the password
|
||||
user_object.username = user_object.username.lower()
|
||||
user_object.password = await security_hash_password(user_object.password)
|
||||
|
||||
# Get org_id from org_slug
|
||||
orgs = request.app.db["organizations"]
|
||||
|
||||
# Check if the org exists
|
||||
isOrgExists = await orgs.find_one({"slug": org_slug})
|
||||
|
||||
# If the org does not exist, raise an error
|
||||
if not isOrgExists:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="You are trying to create a user in an organization that does not exist",
|
||||
)
|
||||
|
||||
org_id = isOrgExists["org_id"]
|
||||
|
||||
# Create initial orgs list with the org_id passed in
|
||||
orgs = [UserOrganization(org_id=org_id, org_role="owner")]
|
||||
|
||||
# Give role
|
||||
roles = [UserRolesInOrganization(role_id="role_admin", org_id=org_id)]
|
||||
|
||||
# Create the user
|
||||
user = UserInDB(
|
||||
user_id=user_id,
|
||||
creation_date=str(datetime.now()),
|
||||
update_date=str(datetime.now()),
|
||||
orgs=orgs,
|
||||
roles=roles,
|
||||
**user_object.dict(),
|
||||
)
|
||||
|
||||
# Insert the user into the database
|
||||
await users.insert_one(user.dict())
|
||||
|
||||
return User(**user.dict())
|
||||
|
||||
|
||||
async def create_sample_data(org_slug: str, username: str, request: Request):
|
||||
Faker(["en_US"])
|
||||
fake_multilang = Faker(
|
||||
["en_US", "de_DE", "ja_JP", "es_ES", "it_IT", "pt_BR", "ar_PS"]
|
||||
)
|
||||
|
||||
users = request.app.db["users"]
|
||||
orgs = request.app.db["organizations"]
|
||||
user = await users.find_one({"username": username})
|
||||
org = await orgs.find_one({"slug": org_slug.lower()})
|
||||
user_id = user["user_id"]
|
||||
org_id = org["org_id"]
|
||||
|
||||
current_user = PublicUser(**user)
|
||||
|
||||
for i in range(0, 5):
|
||||
# get image in BinaryIO format from unsplash and save it to disk
|
||||
image = requests.get("https://source.unsplash.com/random/800x600")
|
||||
with open("thumbnail.jpg", "wb") as f:
|
||||
f.write(image.content)
|
||||
|
||||
course_id = f"course_{uuid4()}"
|
||||
course = CourseInDB(
|
||||
name=fake_multilang.unique.sentence(),
|
||||
description=fake_multilang.unique.text(),
|
||||
mini_description=fake_multilang.unique.text(),
|
||||
thumbnail="thumbnail",
|
||||
org_id=org_id,
|
||||
learnings=[fake_multilang.unique.sentence() for i in range(0, 5)],
|
||||
public=True,
|
||||
chapters=[],
|
||||
course_id=course_id,
|
||||
creationDate=str(datetime.now()),
|
||||
updateDate=str(datetime.now()),
|
||||
authors=[user_id],
|
||||
chapters_content=[],
|
||||
)
|
||||
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
course = CourseInDB(**course.dict())
|
||||
await courses.insert_one(course.dict())
|
||||
|
||||
# create chapters
|
||||
for i in range(0, 5):
|
||||
coursechapter = CourseChapter(
|
||||
name=fake_multilang.unique.sentence(),
|
||||
description=fake_multilang.unique.text(),
|
||||
activities=[],
|
||||
)
|
||||
coursechapter = await create_coursechapter(
|
||||
request, coursechapter, course_id, current_user
|
||||
)
|
||||
if coursechapter:
|
||||
# create activities
|
||||
for i in range(0, 5):
|
||||
activity = Activity(
|
||||
name=fake_multilang.unique.sentence(),
|
||||
type="dynamic",
|
||||
content={},
|
||||
)
|
||||
activity = await create_activity(
|
||||
request,
|
||||
activity,
|
||||
org_id,
|
||||
coursechapter["coursechapter_id"],
|
||||
current_user,
|
||||
)
|
||||
0
apps/api/src/services/orgs/__init__.py
Normal file
0
apps/api/src/services/orgs/__init__.py
Normal file
17
apps/api/src/services/orgs/logos.py
Normal file
17
apps/api/src/services/orgs/logos.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from uuid import uuid4
|
||||
|
||||
from src.services.utils.upload_content import upload_content
|
||||
|
||||
|
||||
async def upload_org_logo(logo_file, org_id):
|
||||
contents = logo_file.file.read()
|
||||
name_in_disk = f"{uuid4()}.{logo_file.filename.split('.')[-1]}"
|
||||
|
||||
await upload_content(
|
||||
"logos",
|
||||
org_id,
|
||||
contents,
|
||||
name_in_disk,
|
||||
)
|
||||
|
||||
return name_in_disk
|
||||
225
apps/api/src/services/orgs/orgs.py
Normal file
225
apps/api/src/services/orgs/orgs.py
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
import json
|
||||
from typing import Literal
|
||||
from uuid import uuid4
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
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 fastapi import HTTPException, UploadFile, status, Request
|
||||
|
||||
|
||||
async def get_organization(request: Request, org_id: 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"
|
||||
)
|
||||
|
||||
org = PublicOrganization(**org)
|
||||
return org
|
||||
|
||||
|
||||
async def get_organization_by_slug(request: Request, org_slug: str):
|
||||
orgs = request.app.db["organizations"]
|
||||
|
||||
org = await orgs.find_one({"slug": org_slug})
|
||||
|
||||
if not org:
|
||||
raise HTTPException(
|
||||
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
|
||||
):
|
||||
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})
|
||||
|
||||
if isOrgAvailable:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Organization slug already exists",
|
||||
)
|
||||
|
||||
# generate org_id with uuid4
|
||||
org_id = str(f"org_{uuid4()}")
|
||||
|
||||
org = OrganizationInDB(org_id=org_id, **org_object.dict())
|
||||
|
||||
org_in_db = await orgs.insert_one(org.dict())
|
||||
|
||||
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()}},
|
||||
)
|
||||
|
||||
# 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",
|
||||
)
|
||||
|
||||
return org.dict()
|
||||
|
||||
|
||||
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")
|
||||
|
||||
orgs = request.app.db["organizations"]
|
||||
|
||||
await orgs.find_one({"org_id": org_id})
|
||||
|
||||
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 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"]
|
||||
|
||||
await orgs.find_one({"org_id": org_id})
|
||||
|
||||
name_in_disk = await upload_org_logo(logo_file, org_id)
|
||||
|
||||
# update 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"]
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
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:
|
||||
raise HTTPException(
|
||||
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
|
||||
):
|
||||
orgs = request.app.db["organizations"]
|
||||
user = request.app.db["users"]
|
||||
|
||||
if user_id == "anonymous":
|
||||
# raise error
|
||||
raise HTTPException(
|
||||
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"
|
||||
):
|
||||
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)
|
||||
]
|
||||
|
||||
|
||||
#### Security ####################################################
|
||||
|
||||
|
||||
async def verify_org_rights(
|
||||
request: Request,
|
||||
org_id: str,
|
||||
current_user: PublicUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
):
|
||||
orgs = request.app.db["organizations"]
|
||||
users = request.app.db["users"]
|
||||
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
await authorization_verify_if_user_is_anon(current_user.user_id)
|
||||
|
||||
await authorization_verify_based_on_roles(
|
||||
request, current_user.user_id, action, user["roles"], org_id
|
||||
)
|
||||
|
||||
|
||||
#### Security ####################################################
|
||||
0
apps/api/src/services/orgs/schemas/__init__.py
Normal file
0
apps/api/src/services/orgs/schemas/__init__.py
Normal file
28
apps/api/src/services/orgs/schemas/orgs.py
Normal file
28
apps/api/src/services/orgs/schemas/orgs.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
#### 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)
|
||||
0
apps/api/src/services/roles/__init__.py
Normal file
0
apps/api/src/services/roles/__init__.py
Normal file
127
apps/api/src/services/roles/roles.py
Normal file
127
apps/api/src/services/roles/roles.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
from typing import Literal
|
||||
from uuid import uuid4
|
||||
from src.security.rbac.rbac import authorization_verify_if_user_is_anon
|
||||
from src.services.roles.schemas.roles import Role, RoleInDB
|
||||
from src.services.users.schemas.users import PublicUser
|
||||
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,
|
||||
):
|
||||
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"
|
||||
)
|
||||
|
||||
await authorization_verify_if_user_is_anon(current_user.user_id)
|
||||
|
||||
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 ####################################################
|
||||
0
apps/api/src/services/roles/schemas/__init__.py
Normal file
0
apps/api/src/services/roles/schemas/__init__.py
Normal file
41
apps/api/src/services/roles/schemas/roles.py
Normal file
41
apps/api/src/services/roles/schemas/roles.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
from typing import 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
|
||||
activities: 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
|
||||
|
||||
0
apps/api/src/services/trail/__init__.py
Normal file
0
apps/api/src/services/trail/__init__.py
Normal file
286
apps/api/src/services/trail/trail.py
Normal file
286
apps/api/src/services/trail/trail.py
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
from datetime import datetime
|
||||
from typing import List, Literal, Optional
|
||||
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.orgs import PublicOrganization
|
||||
|
||||
from src.services.users.users import PublicUser
|
||||
|
||||
#### Classes ####################################################
|
||||
|
||||
|
||||
class ActivityData(BaseModel):
|
||||
activity_id: str
|
||||
activity_type: str
|
||||
data: Optional[dict]
|
||||
|
||||
|
||||
class TrailCourse(BaseModel):
|
||||
course_id: str
|
||||
elements_type: Optional[Literal["course"]] = "course"
|
||||
status: Optional[Literal["ongoing", "done", "closed"]] = "ongoing"
|
||||
course_object: dict
|
||||
masked: Optional[bool] = False
|
||||
activities_marked_complete: Optional[List[str]]
|
||||
activities_data: Optional[List[ActivityData]]
|
||||
progress: Optional[int]
|
||||
|
||||
|
||||
class Trail(BaseModel):
|
||||
status: Optional[Literal["ongoing", "done", "closed"]] = "ongoing"
|
||||
masked: Optional[bool] = False
|
||||
courses: Optional[List[TrailCourse]]
|
||||
|
||||
|
||||
class TrailInDB(Trail):
|
||||
trail_id: str
|
||||
org_id: str
|
||||
user_id: str
|
||||
creationDate: str = datetime.now().isoformat()
|
||||
updateDate: str = datetime.now().isoformat()
|
||||
|
||||
|
||||
#### Classes ####################################################
|
||||
|
||||
|
||||
async def create_trail(
|
||||
request: Request, user: PublicUser, org_id: str, trail_object: Trail
|
||||
) -> Trail:
|
||||
trails = request.app.db["trails"]
|
||||
|
||||
# get list of courses
|
||||
if trail_object.courses:
|
||||
courses = trail_object.courses
|
||||
# get course ids
|
||||
course_ids = [course.course_id for course in courses]
|
||||
|
||||
# find if the user has already started the course
|
||||
existing_trail = await trails.find_one(
|
||||
{"user_id": user.user_id, "courses.course_id": {"$in": course_ids}}
|
||||
)
|
||||
if existing_trail:
|
||||
# update the status of the element with the matching course_id to "ongoing"
|
||||
for element in existing_trail["courses"]:
|
||||
if element["course_id"] in course_ids:
|
||||
element["status"] = "ongoing"
|
||||
# update the existing trail in the database
|
||||
await trails.replace_one(
|
||||
{"trail_id": existing_trail["trail_id"]}, existing_trail
|
||||
)
|
||||
|
||||
# create trail id
|
||||
trail_id = f"trail_{uuid4()}"
|
||||
|
||||
# create trail
|
||||
trail = TrailInDB(
|
||||
**trail_object.dict(), trail_id=trail_id, user_id=user.user_id, org_id=org_id
|
||||
)
|
||||
|
||||
await trails.insert_one(trail.dict())
|
||||
|
||||
return trail
|
||||
|
||||
|
||||
async def get_user_trail(request: Request, org_slug: str, user: PublicUser) -> Trail:
|
||||
trails = request.app.db["trails"]
|
||||
trail = await trails.find_one({"user_id": user.user_id})
|
||||
if not trail:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found"
|
||||
)
|
||||
for element in trail["courses"]:
|
||||
course_id = element["course_id"]
|
||||
chapters_meta = await get_coursechapters_meta(request, course_id, user)
|
||||
activities = chapters_meta["activities"]
|
||||
num_activities = len(activities)
|
||||
|
||||
num_completed_activities = len(element.get("activities_marked_complete", []))
|
||||
element["progress"] = (
|
||||
round((num_completed_activities / num_activities) * 100, 2)
|
||||
if num_activities > 0
|
||||
else 0
|
||||
)
|
||||
|
||||
return Trail(**trail)
|
||||
|
||||
|
||||
async def get_user_trail_with_orgslug(
|
||||
request: Request, user: PublicUser, org_slug: str
|
||||
) -> Trail:
|
||||
trails = request.app.db["trails"]
|
||||
orgs = request.app.db["organizations"]
|
||||
courses_mongo = request.app.db["courses"]
|
||||
|
||||
# get org_id from orgslug
|
||||
org = await orgs.find_one({"slug": org_slug})
|
||||
|
||||
trail = await trails.find_one({"user_id": user.user_id, "org_id": org["org_id"]})
|
||||
|
||||
if not trail:
|
||||
return Trail(masked=False, courses=[])
|
||||
|
||||
course_ids = [course["course_id"] for course in trail["courses"]]
|
||||
|
||||
live_courses = await courses_mongo.find({"course_id": {"$in": course_ids}}).to_list(
|
||||
length=None
|
||||
)
|
||||
|
||||
for course in trail["courses"]:
|
||||
course_id = course["course_id"]
|
||||
|
||||
if course_id not in [course["course_id"] for course in live_courses]:
|
||||
course["masked"] = True
|
||||
continue
|
||||
|
||||
chapters_meta = await get_coursechapters_meta(request, course_id, user)
|
||||
activities = chapters_meta["activities"]
|
||||
|
||||
# get course object without _id
|
||||
course_object = await courses_mongo.find_one(
|
||||
{"course_id": course_id}, {"_id": 0}
|
||||
)
|
||||
|
||||
course["course_object"] = course_object
|
||||
num_activities = len(activities)
|
||||
|
||||
num_completed_activities = len(course.get("activities_marked_complete", []))
|
||||
course["progress"] = (
|
||||
round((num_completed_activities / num_activities) * 100, 2)
|
||||
if num_activities > 0
|
||||
else 0
|
||||
)
|
||||
|
||||
return Trail(**trail)
|
||||
|
||||
|
||||
async def add_activity_to_trail(
|
||||
request: Request, user: PublicUser, course_id: str, org_slug: str, activity_id: str
|
||||
) -> Trail:
|
||||
trails = request.app.db["trails"]
|
||||
orgs = request.app.db["organizations"]
|
||||
courseid = "course_" + course_id
|
||||
activityid = "activity_" + activity_id
|
||||
|
||||
# get org_id from orgslug
|
||||
org = await orgs.find_one({"slug": org_slug})
|
||||
org_id = org["org_id"]
|
||||
|
||||
# find a trail with the user_id and course_id in the courses array
|
||||
trail = await trails.find_one(
|
||||
{"user_id": user.user_id, "courses.course_id": courseid, "org_id": org_id}
|
||||
)
|
||||
|
||||
if user.user_id == "anonymous":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Anonymous users cannot add activity to trail",
|
||||
)
|
||||
|
||||
if not trail:
|
||||
return Trail(masked=False, courses=[])
|
||||
|
||||
# if a trail has course_id in the courses array, then add the activity_id to the activities_marked_complete array
|
||||
for element in trail["courses"]:
|
||||
if element["course_id"] == courseid:
|
||||
if "activities_marked_complete" in element:
|
||||
# check if activity_id is already in the array
|
||||
if activityid not in element["activities_marked_complete"]:
|
||||
element["activities_marked_complete"].append(activityid)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Activity already marked complete",
|
||||
)
|
||||
else:
|
||||
element["activities_marked_complete"] = [activity_id]
|
||||
|
||||
# modify trail object
|
||||
await trails.replace_one({"trail_id": trail["trail_id"]}, trail)
|
||||
|
||||
return Trail(**trail)
|
||||
|
||||
|
||||
async def add_course_to_trail(
|
||||
request: Request, user: PublicUser, orgslug: str, course_id: str
|
||||
) -> Trail:
|
||||
trails = request.app.db["trails"]
|
||||
orgs = request.app.db["organizations"]
|
||||
|
||||
if user.user_id == "anonymous":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Anonymous users cannot add activity to trail",
|
||||
)
|
||||
|
||||
org = await orgs.find_one({"slug": orgslug})
|
||||
|
||||
org = PublicOrganization(**org)
|
||||
|
||||
trail = await trails.find_one({"user_id": user.user_id, "org_id": org["org_id"]})
|
||||
|
||||
if not trail:
|
||||
trail_to_insert = TrailInDB(
|
||||
trail_id=f"trail_{uuid4()}",
|
||||
user_id=user.user_id,
|
||||
org_id=org["org_id"],
|
||||
courses=[],
|
||||
)
|
||||
trail_to_insert = await trails.insert_one(trail_to_insert.dict())
|
||||
|
||||
trail = await trails.find_one({"_id": trail_to_insert.inserted_id})
|
||||
|
||||
# check if course is already present in the trail
|
||||
for element in trail["courses"]:
|
||||
if element["course_id"] == course_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Course already present in the trail",
|
||||
)
|
||||
|
||||
updated_trail = TrailCourse(
|
||||
course_id=course_id,
|
||||
activities_data=[],
|
||||
activities_marked_complete=[],
|
||||
progress=0,
|
||||
course_object={},
|
||||
status="ongoing",
|
||||
masked=False,
|
||||
)
|
||||
trail["courses"].append(updated_trail.dict())
|
||||
await trails.replace_one({"trail_id": trail["trail_id"]}, trail)
|
||||
return Trail(**trail)
|
||||
|
||||
|
||||
async def remove_course_from_trail(
|
||||
request: Request, user: PublicUser, orgslug: str, course_id: str
|
||||
) -> Trail:
|
||||
trails = request.app.db["trails"]
|
||||
orgs = request.app.db["organizations"]
|
||||
|
||||
if user.user_id == "anonymous":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Anonymous users cannot add activity to trail",
|
||||
)
|
||||
|
||||
org = await orgs.find_one({"slug": orgslug})
|
||||
|
||||
org = PublicOrganization(**org)
|
||||
trail = await trails.find_one({"user_id": user.user_id, "org_id": org["org_id"]})
|
||||
|
||||
if not trail:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found"
|
||||
)
|
||||
|
||||
# check if course is already present in the trail
|
||||
|
||||
for element in trail["courses"]:
|
||||
if element["course_id"] == course_id:
|
||||
trail["courses"].remove(element)
|
||||
break
|
||||
|
||||
await trails.replace_one({"trail_id": trail["trail_id"]}, trail)
|
||||
return Trail(**trail)
|
||||
0
apps/api/src/services/users/__init__.py
Normal file
0
apps/api/src/services/users/__init__.py
Normal file
0
apps/api/src/services/users/schemas/__init__.py
Normal file
0
apps/api/src/services/users/schemas/__init__.py
Normal file
70
apps/api/src/services/users/schemas/users.py
Normal file
70
apps/api/src/services/users/schemas/users.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
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 UserRolesInOrganization(BaseModel):
|
||||
org_id: str
|
||||
role_id: str
|
||||
|
||||
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[UserRolesInOrganization] = []
|
||||
creation_date: str
|
||||
update_date: str
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
|
||||
|
||||
|
||||
|
||||
class PublicUser(User):
|
||||
user_id: str
|
||||
orgs: list[UserOrganization] = []
|
||||
roles: list[UserRolesInOrganization] = []
|
||||
creation_date: str
|
||||
update_date: str
|
||||
|
||||
class AnonymousUser(BaseModel):
|
||||
user_id: str = "anonymous"
|
||||
username: str = "anonymous"
|
||||
roles: list[UserRolesInOrganization] = [
|
||||
UserRolesInOrganization(org_id="anonymous", role_id="role_anonymous")
|
||||
]
|
||||
|
||||
|
||||
|
||||
# Forms ####################################################
|
||||
|
||||
class PasswordChangeForm(BaseModel):
|
||||
old_password: str
|
||||
new_password: str
|
||||
321
apps/api/src/services/users/users.py
Normal file
321
apps/api/src/services/users/users.py
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
from datetime import datetime
|
||||
from typing import Literal
|
||||
from uuid import uuid4
|
||||
from fastapi import HTTPException, Request, status
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
from src.security.security import security_hash_password, security_verify_password
|
||||
from src.services.users.schemas.users import (
|
||||
PasswordChangeForm,
|
||||
PublicUser,
|
||||
User,
|
||||
UserOrganization,
|
||||
UserRolesInOrganization,
|
||||
UserWithPassword,
|
||||
UserInDB,
|
||||
)
|
||||
|
||||
|
||||
async def create_user(
|
||||
request: Request,
|
||||
current_user: PublicUser | None,
|
||||
user_object: UserWithPassword,
|
||||
org_slug: str,
|
||||
):
|
||||
users = request.app.db["users"]
|
||||
|
||||
isUsernameAvailable = await users.find_one({"username": user_object.username})
|
||||
isEmailAvailable = await users.find_one({"email": user_object.email})
|
||||
|
||||
if isUsernameAvailable:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Username already exists"
|
||||
)
|
||||
|
||||
if isEmailAvailable:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Email already exists"
|
||||
)
|
||||
|
||||
# Generate user_id with uuid4
|
||||
user_id = str(f"user_{uuid4()}")
|
||||
|
||||
# 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)
|
||||
|
||||
# Get org_id from org_slug
|
||||
orgs = request.app.db["organizations"]
|
||||
|
||||
# Check if the org exists
|
||||
isOrgExists = await orgs.find_one({"slug": org_slug})
|
||||
|
||||
# If the org does not exist, raise an error
|
||||
if not isOrgExists and (org_slug != "None"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="You are trying to create a user in an organization that does not exist",
|
||||
)
|
||||
|
||||
org_id = isOrgExists["org_id"] if org_slug != "None" else ''
|
||||
|
||||
# Create initial orgs list with the org_id passed in
|
||||
orgs = (
|
||||
[UserOrganization(org_id=org_id, org_role="member")]
|
||||
if org_slug != "None"
|
||||
else []
|
||||
)
|
||||
|
||||
# Give role
|
||||
roles = (
|
||||
[UserRolesInOrganization(role_id="role_member", org_id=org_id)]
|
||||
if org_slug != "None"
|
||||
else []
|
||||
)
|
||||
|
||||
# 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"]
|
||||
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":
|
||||
await authorization_verify_if_user_is_anon(current_user.user_id)
|
||||
|
||||
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":
|
||||
await authorization_verify_if_user_is_anon(current_user.user_id)
|
||||
|
||||
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
|
||||
|
||||
await authorization_verify_based_on_roles(
|
||||
request, current_user.user_id, "update", user["roles"], user_id
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
if action == "delete":
|
||||
await authorization_verify_if_user_is_anon(current_user.user_id)
|
||||
|
||||
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
|
||||
|
||||
await authorization_verify_based_on_roles(
|
||||
request, current_user.user_id, "update", user["roles"], user_id
|
||||
)
|
||||
70
apps/api/src/services/utils/upload_content.py
Normal file
70
apps/api/src/services/utils/upload_content.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
import os
|
||||
|
||||
from config.config import get_learnhouse_config
|
||||
|
||||
|
||||
async def upload_content(
|
||||
directory: str, org_id: str, file_binary: bytes, file_and_format: str
|
||||
):
|
||||
# Get Learnhouse Config
|
||||
learnhouse_config = get_learnhouse_config()
|
||||
|
||||
# Get content delivery method
|
||||
content_delivery = learnhouse_config.hosting_config.content_delivery.type
|
||||
|
||||
if content_delivery == "filesystem":
|
||||
# create folder for activity
|
||||
if not os.path.exists(f"content/{org_id}/{directory}"):
|
||||
# create folder for activity
|
||||
os.makedirs(f"content/{org_id}/{directory}")
|
||||
# upload file to server
|
||||
with open(
|
||||
f"content/{org_id}/{directory}/{file_and_format}",
|
||||
"wb",
|
||||
) as f:
|
||||
f.write(file_binary)
|
||||
f.close()
|
||||
|
||||
elif content_delivery == "s3api":
|
||||
# Upload to server then to s3 (AWS Keys are stored in environment variables and are loaded by boto3)
|
||||
# TODO: Improve implementation of this
|
||||
print("Uploading to s3...")
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
endpoint_url=learnhouse_config.hosting_config.content_delivery.s3api.endpoint_url,
|
||||
)
|
||||
|
||||
# Create folder for activity
|
||||
if not os.path.exists(f"content/{org_id}/{directory}"):
|
||||
# create folder for activity
|
||||
os.makedirs(f"content/{org_id}/{directory}")
|
||||
|
||||
# Upload file to server
|
||||
with open(
|
||||
f"content/{org_id}/{directory}/{file_and_format}",
|
||||
"wb",
|
||||
) as f:
|
||||
f.write(file_binary)
|
||||
f.close()
|
||||
|
||||
print("Uploading to s3 using boto3...")
|
||||
try:
|
||||
s3.upload_file(
|
||||
f"content/{org_id}/{directory}/{file_and_format}",
|
||||
"learnhouse-media",
|
||||
f"content/{org_id}/{directory}/{file_and_format}",
|
||||
)
|
||||
except ClientError as e:
|
||||
print(e)
|
||||
|
||||
print("Checking if file exists in s3...")
|
||||
try:
|
||||
s3.head_object(
|
||||
Bucket="learnhouse-media",
|
||||
Key=f"content/{org_id}/{directory}/{file_and_format}",
|
||||
)
|
||||
print("File upload successful!")
|
||||
except Exception as e:
|
||||
print(f"An error occurred: {str(e)}")
|
||||
Loading…
Add table
Add a link
Reference in a new issue