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/__init__.py
Normal file
0
apps/api/src/__init__.py
Normal file
0
apps/api/src/core/__init__.py
Normal file
0
apps/api/src/core/__init__.py
Normal file
0
apps/api/src/core/events/__init__.py
Normal file
0
apps/api/src/core/events/__init__.py
Normal file
8
apps/api/src/core/events/content.py
Normal file
8
apps/api/src/core/events/content.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import os
|
||||
|
||||
|
||||
async def check_content_directory():
|
||||
if not os.path.exists("content"):
|
||||
# create folder for activity
|
||||
print("Creating content directory...")
|
||||
os.makedirs("content")
|
||||
21
apps/api/src/core/events/database.py
Normal file
21
apps/api/src/core/events/database.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import logging
|
||||
from fastapi import FastAPI
|
||||
import motor.motor_asyncio
|
||||
|
||||
|
||||
async def connect_to_db(app: FastAPI):
|
||||
logging.info("Connecting to database...")
|
||||
try:
|
||||
app.mongodb_client = motor.motor_asyncio.AsyncIOMotorClient( # type: ignore
|
||||
app.learnhouse_config.database_config.mongodb_connection_string) # type: ignore
|
||||
app.db = app.mongodb_client["learnhouse"] # type: ignore
|
||||
logging.info("Connected to database!")
|
||||
except Exception as e:
|
||||
logging.error("Failed to connect to database!")
|
||||
logging.error(e)
|
||||
|
||||
|
||||
async def close_database(app: FastAPI):
|
||||
app.mongodb_client.close() # type: ignore
|
||||
logging.info("LearnHouse has been shut down.")
|
||||
return app
|
||||
35
apps/api/src/core/events/events.py
Normal file
35
apps/api/src/core/events/events.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
from typing import Callable
|
||||
from fastapi import FastAPI
|
||||
from config.config import LearnHouseConfig, get_learnhouse_config
|
||||
from src.core.events.content import check_content_directory
|
||||
from src.core.events.database import close_database, connect_to_db
|
||||
from src.core.events.logs import create_logs_dir
|
||||
from src.core.events.sentry import init_sentry
|
||||
|
||||
|
||||
def startup_app(app: FastAPI) -> Callable:
|
||||
async def start_app() -> None:
|
||||
# Get LearnHouse Config
|
||||
learnhouse_config: LearnHouseConfig = get_learnhouse_config()
|
||||
app.learnhouse_config = learnhouse_config # type: ignore
|
||||
|
||||
# Init Sentry
|
||||
await init_sentry(app)
|
||||
|
||||
# Connect to database
|
||||
await connect_to_db(app)
|
||||
|
||||
# Create logs directory
|
||||
await create_logs_dir()
|
||||
|
||||
# Create content directory
|
||||
await check_content_directory()
|
||||
|
||||
return start_app
|
||||
|
||||
|
||||
def shutdown_app(app: FastAPI) -> Callable:
|
||||
async def close_app() -> None:
|
||||
await close_database(app)
|
||||
|
||||
return close_app
|
||||
24
apps/api/src/core/events/logs.py
Normal file
24
apps/api/src/core/events/logs.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import logging
|
||||
import os
|
||||
|
||||
|
||||
async def create_logs_dir():
|
||||
if not os.path.exists("logs"):
|
||||
os.mkdir("logs")
|
||||
|
||||
# Initiate logging
|
||||
async def init_logging():
|
||||
await create_logs_dir()
|
||||
|
||||
# Logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
datefmt="%d-%b-%y %H:%M:%S",
|
||||
handlers=[
|
||||
logging.FileHandler("logs/learnhouse.log"),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
|
||||
logging.info("Logging initiated")
|
||||
16
apps/api/src/core/events/sentry.py
Normal file
16
apps/api/src/core/events/sentry.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from fastapi import FastAPI
|
||||
|
||||
import sentry_sdk
|
||||
|
||||
from config.config import LearnHouseConfig
|
||||
|
||||
async def init_sentry(app: FastAPI) -> None:
|
||||
|
||||
leanrhouse_config : LearnHouseConfig = app.learnhouse_config # type: ignore
|
||||
if leanrhouse_config.hosting_config.sentry_config is not None:
|
||||
sentry_sdk.init(
|
||||
dsn=app.learnhouse_config.hosting_config.sentry_config.dsn, # type: ignore
|
||||
environment=app.learnhouse_config.hosting_config.sentry_config.environment, # type: ignore
|
||||
release=app.learnhouse_config.hosting_config.sentry_config.release, # type: ignore
|
||||
traces_sample_rate=1.0,
|
||||
)
|
||||
37
apps/api/src/router.py
Normal file
37
apps/api/src/router.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
from fastapi import APIRouter, Depends
|
||||
from src.routers import blocks, dev, trail, users, auth, orgs, roles
|
||||
from src.routers.courses import chapters, collections, courses, activities
|
||||
from src.routers.install import install
|
||||
from src.services.dev.dev import isDevModeEnabledOrRaise
|
||||
from src.services.install.install import isInstallModeEnabled
|
||||
|
||||
|
||||
v1_router = APIRouter(prefix="/api/v1")
|
||||
|
||||
|
||||
# API Routes
|
||||
v1_router.include_router(users.router, prefix="/users", tags=["users"])
|
||||
v1_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||
v1_router.include_router(orgs.router, prefix="/orgs", tags=["orgs"])
|
||||
v1_router.include_router(roles.router, prefix="/roles", tags=["roles"])
|
||||
v1_router.include_router(blocks.router, prefix="/blocks", tags=["blocks"])
|
||||
v1_router.include_router(courses.router, prefix="/courses", tags=["courses"])
|
||||
v1_router.include_router(chapters.router, prefix="/chapters", tags=["chapters"])
|
||||
v1_router.include_router(activities.router, prefix="/activities", tags=["activities"])
|
||||
v1_router.include_router(
|
||||
collections.router, prefix="/collections", tags=["collections"]
|
||||
)
|
||||
v1_router.include_router(trail.router, prefix="/trail", tags=["trail"])
|
||||
|
||||
# Dev Routes
|
||||
v1_router.include_router(
|
||||
dev.router, prefix="/dev", tags=["dev"], dependencies=[Depends(isDevModeEnabledOrRaise)]
|
||||
)
|
||||
|
||||
# Install Routes
|
||||
v1_router.include_router(
|
||||
install.router,
|
||||
prefix="/install",
|
||||
tags=["install"],
|
||||
dependencies=[Depends(isInstallModeEnabled)],
|
||||
)
|
||||
0
apps/api/src/routers/__init__.py
Normal file
0
apps/api/src/routers/__init__.py
Normal file
67
apps/api/src/routers/auth.py
Normal file
67
apps/api/src/routers/auth.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
from fastapi import Depends, APIRouter, HTTPException, Response, status, Request
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from config.config import get_learnhouse_config
|
||||
from src.security.auth import AuthJWT, authenticate_user
|
||||
from src.services.users.users import PublicUser
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/refresh")
|
||||
def refresh(response: Response,Authorize: AuthJWT = Depends()):
|
||||
"""
|
||||
The jwt_refresh_token_required() function insures a valid refresh
|
||||
token is present in the request before running any code below that function.
|
||||
we can use the get_jwt_subject() function to get the subject of the refresh
|
||||
token, and use the create_access_token() function again to make a new access token
|
||||
"""
|
||||
Authorize.jwt_refresh_token_required()
|
||||
|
||||
current_user = Authorize.get_jwt_subject()
|
||||
new_access_token = Authorize.create_access_token(subject=current_user) # type: ignore
|
||||
|
||||
response.set_cookie(key="access_token_cookie", value=new_access_token, httponly=False, domain=get_learnhouse_config().hosting_config.cookie_config.domain)
|
||||
return {"access_token": new_access_token}
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
async def login(
|
||||
request: Request,
|
||||
response: Response,
|
||||
Authorize: AuthJWT = Depends(),
|
||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||
):
|
||||
user = await authenticate_user(request, form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect Email or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
access_token = Authorize.create_access_token(subject=form_data.username)
|
||||
refresh_token = Authorize.create_refresh_token(subject=form_data.username)
|
||||
Authorize.set_refresh_cookies(refresh_token)
|
||||
# set cookies using fastapi
|
||||
response.set_cookie(key="access_token_cookie", value=access_token, httponly=False, domain=get_learnhouse_config().hosting_config.cookie_config.domain)
|
||||
user = PublicUser(**user.dict())
|
||||
|
||||
result = {
|
||||
"user": user,
|
||||
"tokens": {"access_token": access_token, "refresh_token": refresh_token},
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
@router.delete("/logout")
|
||||
def logout(Authorize: AuthJWT = Depends()):
|
||||
"""
|
||||
Because the JWT are stored in an httponly cookie now, we cannot
|
||||
log the user out by simply deleting the cookies in the frontend.
|
||||
We need the backend to send us a response to delete the cookies.
|
||||
"""
|
||||
Authorize.jwt_required()
|
||||
|
||||
Authorize.unset_jwt_cookies()
|
||||
return {"msg": "Successfully logout"}
|
||||
94
apps/api/src/routers/blocks.py
Normal file
94
apps/api/src/routers/blocks.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
from fastapi import APIRouter, Depends, UploadFile, Form, Request
|
||||
from src.security.auth import get_current_user
|
||||
from src.services.blocks.block_types.imageBlock.images import create_image_block, get_image_block
|
||||
from src.services.blocks.block_types.videoBlock.videoBlock import create_video_block, get_video_block
|
||||
from src.services.blocks.block_types.pdfBlock.pdfBlock import create_pdf_block, get_pdf_block
|
||||
from src.services.blocks.block_types.quizBlock.quizBlock import create_quiz_block, get_quiz_block_answers, get_quiz_block_options, quizBlock
|
||||
from src.services.users.users import PublicUser
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
####################
|
||||
# Image Block
|
||||
####################
|
||||
|
||||
@router.post("/image")
|
||||
async def api_create_image_file_block(request: Request, file_object: UploadFile, activity_id: str = Form(), current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Create new image file
|
||||
"""
|
||||
return await create_image_block(request, file_object, activity_id)
|
||||
|
||||
|
||||
@router.get("/image")
|
||||
async def api_get_image_file_block(request: Request, file_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get image file
|
||||
"""
|
||||
return await get_image_block(request, file_id, current_user)
|
||||
|
||||
####################
|
||||
# Video Block
|
||||
####################
|
||||
|
||||
@router.post("/video")
|
||||
async def api_create_video_file_block(request: Request, file_object: UploadFile, activity_id: str = Form(), current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Create new video file
|
||||
"""
|
||||
return await create_video_block(request, file_object, activity_id)
|
||||
|
||||
|
||||
@router.get("/video")
|
||||
async def api_get_video_file_block(request: Request, file_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get video file
|
||||
"""
|
||||
return await get_video_block(request, file_id, current_user)
|
||||
|
||||
####################
|
||||
# PDF Block
|
||||
####################
|
||||
|
||||
@router.post("/pdf")
|
||||
async def api_create_pdf_file_block(request: Request, file_object: UploadFile, activity_id: str = Form(), current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Create new pdf file
|
||||
"""
|
||||
return await create_pdf_block(request, file_object, activity_id)
|
||||
|
||||
|
||||
@router.get("/pdf")
|
||||
async def api_get_pdf_file_block(request: Request, file_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get pdf file
|
||||
"""
|
||||
return await get_pdf_block(request, file_id, current_user)
|
||||
|
||||
|
||||
####################
|
||||
# Quiz Block
|
||||
####################
|
||||
|
||||
@router.post("/quiz/{activity_id}")
|
||||
async def api_create_quiz_block(request: Request, quiz_block: quizBlock, activity_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Create new document file
|
||||
"""
|
||||
return await create_quiz_block(request, quiz_block, activity_id, current_user)
|
||||
|
||||
|
||||
@router.get("/quiz/options")
|
||||
async def api_get_quiz_options(request: Request, block_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get quiz options
|
||||
"""
|
||||
return await get_quiz_block_options(request, block_id, current_user)
|
||||
|
||||
|
||||
@router.get("/quiz/answers")
|
||||
async def api_get_quiz_answers(request: Request, block_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get quiz answers
|
||||
"""
|
||||
return await get_quiz_block_answers(request, block_id, current_user)
|
||||
131
apps/api/src/routers/courses/activities.py
Normal file
131
apps/api/src/routers/courses/activities.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
from fastapi import APIRouter, Depends, UploadFile, Form, Request
|
||||
from src.services.courses.activities.activities import (
|
||||
Activity,
|
||||
create_activity,
|
||||
get_activity,
|
||||
get_activities,
|
||||
update_activity,
|
||||
delete_activity,
|
||||
)
|
||||
from src.security.auth import get_current_user
|
||||
from src.services.courses.activities.pdf import create_documentpdf_activity
|
||||
from src.services.courses.activities.video import (
|
||||
ExternalVideo,
|
||||
create_external_video_activity,
|
||||
create_video_activity,
|
||||
)
|
||||
from src.services.users.schemas.users import PublicUser
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def api_create_activity(
|
||||
request: Request,
|
||||
activity_object: Activity,
|
||||
org_id: str,
|
||||
coursechapter_id: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Create new activity
|
||||
"""
|
||||
return await create_activity(
|
||||
request, activity_object, org_id, coursechapter_id, current_user
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{activity_id}")
|
||||
async def api_get_activity(
|
||||
request: Request,
|
||||
activity_id: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Get single activity by activity_id
|
||||
"""
|
||||
return await get_activity(request, activity_id, current_user=current_user)
|
||||
|
||||
|
||||
@router.get("/coursechapter/{coursechapter_id}")
|
||||
async def api_get_activities(
|
||||
request: Request,
|
||||
coursechapter_id: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Get CourseChapter activities
|
||||
"""
|
||||
return await get_activities(request, coursechapter_id, current_user)
|
||||
|
||||
|
||||
@router.put("/{activity_id}")
|
||||
async def api_update_activity(
|
||||
request: Request,
|
||||
activity_object: Activity,
|
||||
activity_id: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Update activity by activity_id
|
||||
"""
|
||||
return await update_activity(request, activity_object, activity_id, current_user)
|
||||
|
||||
|
||||
@router.delete("/{activity_id}")
|
||||
async def api_delete_activity(
|
||||
request: Request,
|
||||
activity_id: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Delete activity by activity_id
|
||||
"""
|
||||
return await delete_activity(request, activity_id, current_user)
|
||||
|
||||
|
||||
# Video activity
|
||||
|
||||
|
||||
@router.post("/video")
|
||||
async def api_create_video_activity(
|
||||
request: Request,
|
||||
name: str = Form(),
|
||||
coursechapter_id: str = Form(),
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
video_file: UploadFile | None = None,
|
||||
):
|
||||
"""
|
||||
Create new activity
|
||||
"""
|
||||
return await create_video_activity(
|
||||
request, name, coursechapter_id, current_user, video_file
|
||||
)
|
||||
|
||||
|
||||
@router.post("/external_video")
|
||||
async def api_create_external_video_activity(
|
||||
request: Request,
|
||||
external_video: ExternalVideo,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Create new activity
|
||||
"""
|
||||
return await create_external_video_activity(request, current_user, external_video)
|
||||
|
||||
|
||||
@router.post("/documentpdf")
|
||||
async def api_create_documentpdf_activity(
|
||||
request: Request,
|
||||
name: str = Form(),
|
||||
coursechapter_id: str = Form(),
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
pdf_file: UploadFile | None = None,
|
||||
):
|
||||
"""
|
||||
Create new activity
|
||||
"""
|
||||
return await create_documentpdf_activity(
|
||||
request, name, coursechapter_id, current_user, pdf_file
|
||||
)
|
||||
64
apps/api/src/routers/courses/chapters.py
Normal file
64
apps/api/src/routers/courses/chapters.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
from fastapi import APIRouter, Depends, Request
|
||||
|
||||
from src.services.courses.chapters import CourseChapter, CourseChapterMetaData, create_coursechapter, delete_coursechapter, get_coursechapter, get_coursechapters, get_coursechapters_meta, update_coursechapter, update_coursechapters_meta
|
||||
from src.services.users.users import PublicUser
|
||||
from src.security.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def api_create_coursechapter(request: Request,coursechapter_object: CourseChapter, course_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Create new CourseChapter
|
||||
"""
|
||||
return await create_coursechapter(request, coursechapter_object, course_id, current_user)
|
||||
|
||||
|
||||
@router.get("/{coursechapter_id}")
|
||||
async def api_get_coursechapter(request: Request,coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get single CourseChapter by coursechapter_id
|
||||
"""
|
||||
return await get_coursechapter(request, coursechapter_id, current_user=current_user)
|
||||
|
||||
|
||||
@router.get("/meta/{course_id}")
|
||||
async def api_get_coursechapter_meta(request: Request,course_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get coursechapter metadata
|
||||
"""
|
||||
return await get_coursechapters_meta(request, course_id, current_user=current_user)
|
||||
|
||||
|
||||
@router.put("/meta/{course_id}")
|
||||
async def api_update_coursechapter_meta(request: Request,course_id: str, coursechapters_metadata: CourseChapterMetaData, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Update coursechapter metadata
|
||||
"""
|
||||
return await update_coursechapters_meta(request, course_id, coursechapters_metadata, current_user=current_user)
|
||||
|
||||
|
||||
@router.get("/{course_id}/page/{page}/limit/{limit}")
|
||||
async def api_get_coursechapter_by(request: Request,course_id: str, page: int, limit: int):
|
||||
"""
|
||||
Get CourseChapters by page and limit
|
||||
"""
|
||||
return await get_coursechapters(request, course_id, page, limit)
|
||||
|
||||
|
||||
@router.put("/{coursechapter_id}")
|
||||
async def api_update_coursechapter(request: Request,coursechapter_object: CourseChapter, coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Update CourseChapters by course_id
|
||||
"""
|
||||
return await update_coursechapter(request, coursechapter_object, coursechapter_id, current_user)
|
||||
|
||||
|
||||
@router.delete("/{coursechapter_id}")
|
||||
async def api_delete_coursechapter(request: Request,coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Delete CourseChapters by ID
|
||||
"""
|
||||
|
||||
return await delete_coursechapter(request,coursechapter_id, current_user)
|
||||
80
apps/api/src/routers/courses/collections.py
Normal file
80
apps/api/src/routers/courses/collections.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
from fastapi import APIRouter, Depends, Request
|
||||
from src.security.auth import get_current_user
|
||||
from src.services.users.users import PublicUser
|
||||
from src.services.courses.collections import (
|
||||
Collection,
|
||||
create_collection,
|
||||
get_collection,
|
||||
get_collections,
|
||||
update_collection,
|
||||
delete_collection,
|
||||
)
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def api_create_collection(
|
||||
request: Request,
|
||||
collection_object: Collection,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Create new Collection
|
||||
"""
|
||||
return await create_collection(request, collection_object, current_user)
|
||||
|
||||
|
||||
@router.get("/{collection_id}")
|
||||
async def api_get_collection(
|
||||
request: Request,
|
||||
collection_id: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Get single collection by ID
|
||||
"""
|
||||
return await get_collection(request, collection_id, current_user)
|
||||
|
||||
|
||||
@router.get("/org_id/{org_id}/page/{page}/limit/{limit}")
|
||||
async def api_get_collections_by(
|
||||
request: Request,
|
||||
page: int,
|
||||
limit: int,
|
||||
org_id: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Get collections by page and limit
|
||||
"""
|
||||
return await get_collections(request, org_id, current_user, page, limit)
|
||||
|
||||
|
||||
@router.put("/{collection_id}")
|
||||
async def api_update_collection(
|
||||
request: Request,
|
||||
collection_object: Collection,
|
||||
collection_id: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Update collection by ID
|
||||
"""
|
||||
return await update_collection(
|
||||
request, collection_object, collection_id, current_user
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{collection_id}")
|
||||
async def api_delete_collection(
|
||||
request: Request,
|
||||
collection_id: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Delete collection by ID
|
||||
"""
|
||||
|
||||
return await delete_collection(request, collection_id, current_user)
|
||||
66
apps/api/src/routers/courses/courses.py
Normal file
66
apps/api/src/routers/courses/courses.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
from fastapi import APIRouter, Depends, UploadFile, Form, Request
|
||||
from src.security.auth import get_current_user
|
||||
|
||||
from src.services.courses.courses import Course, create_course, get_course, get_course_meta, get_courses_orgslug, update_course, delete_course, update_course_thumbnail
|
||||
from src.services.users.users import PublicUser
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def api_create_course(request: Request, org_id: str, name: str = Form(), mini_description: str = Form(), description: str = Form(), public: bool = Form(), current_user: PublicUser = Depends(get_current_user), thumbnail: UploadFile | None = None):
|
||||
"""
|
||||
Create new Course
|
||||
"""
|
||||
course = Course(name=name, mini_description=mini_description, description=description,
|
||||
org_id=org_id, public=public, thumbnail="", chapters=[], chapters_content=[], learnings=[])
|
||||
return await create_course(request, course, org_id, current_user, thumbnail)
|
||||
|
||||
|
||||
@router.put("/thumbnail/{course_id}")
|
||||
async def api_create_course_thumbnail(request: Request, course_id: str, thumbnail: UploadFile | None = None, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Update new Course Thumbnail
|
||||
"""
|
||||
return await update_course_thumbnail(request, course_id, current_user, thumbnail)
|
||||
|
||||
|
||||
@router.get("/{course_id}")
|
||||
async def api_get_course(request: Request, course_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get single Course by course_id
|
||||
"""
|
||||
return await get_course(request, course_id, current_user=current_user)
|
||||
|
||||
|
||||
@router.get("/meta/{course_id}")
|
||||
async def api_get_course_meta(request: Request, course_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get single Course Metadata (chapters, activities) by course_id
|
||||
"""
|
||||
return await get_course_meta(request, course_id, current_user=current_user)
|
||||
|
||||
@router.get("/org_slug/{org_slug}/page/{page}/limit/{limit}")
|
||||
async def api_get_course_by_orgslug(request: Request, page: int, limit: int, org_slug: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get houses by page and limit
|
||||
"""
|
||||
return await get_courses_orgslug(request, current_user, page, limit, org_slug)
|
||||
|
||||
|
||||
@router.put("/{course_id}")
|
||||
async def api_update_course(request: Request, course_object: Course, course_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Update Course by course_id
|
||||
"""
|
||||
return await update_course(request, course_object, course_id, current_user)
|
||||
|
||||
|
||||
@router.delete("/{course_id}")
|
||||
async def api_delete_course(request: Request, course_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Delete Course by ID
|
||||
"""
|
||||
|
||||
return await delete_course(request, course_id, current_user)
|
||||
18
apps/api/src/routers/dev.py
Normal file
18
apps/api/src/routers/dev.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
from fastapi import APIRouter, Request
|
||||
from config.config import get_learnhouse_config
|
||||
from src.services.dev.mocks.initial import create_initial_data
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/config")
|
||||
async def config():
|
||||
config = get_learnhouse_config()
|
||||
return config.dict()
|
||||
|
||||
|
||||
@router.get("/mock/initial")
|
||||
async def initial_data(request: Request):
|
||||
await create_initial_data(request)
|
||||
return {"Message": "Initial data created 🤖"}
|
||||
0
apps/api/src/routers/install/__init__.py
Normal file
0
apps/api/src/routers/install/__init__.py
Normal file
70
apps/api/src/routers/install/install.py
Normal file
70
apps/api/src/routers/install/install.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
from fastapi import APIRouter, Request
|
||||
|
||||
from src.services.install.install import (
|
||||
create_install_instance,
|
||||
create_sample_data,
|
||||
get_latest_install_instance,
|
||||
install_create_organization,
|
||||
install_create_organization_user,
|
||||
install_default_elements,
|
||||
update_install_instance,
|
||||
)
|
||||
from src.services.orgs.schemas.orgs import Organization
|
||||
from src.services.users.schemas.users import UserWithPassword
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/start")
|
||||
async def api_create_install_instance(request: Request, data: dict):
|
||||
# create install
|
||||
install = await create_install_instance(request, data)
|
||||
|
||||
return install
|
||||
|
||||
|
||||
@router.get("/latest")
|
||||
async def api_get_latest_install_instance(request: Request):
|
||||
# get latest created install
|
||||
install = await get_latest_install_instance(request)
|
||||
|
||||
return install
|
||||
|
||||
|
||||
@router.post("/default_elements")
|
||||
async def api_install_def_elements(request: Request):
|
||||
elements = await install_default_elements(request, {})
|
||||
|
||||
return elements
|
||||
|
||||
|
||||
@router.post("/org")
|
||||
async def api_install_org(request: Request, org: Organization):
|
||||
organization = await install_create_organization(request, org)
|
||||
|
||||
return organization
|
||||
|
||||
|
||||
@router.post("/user")
|
||||
async def api_install_user(request: Request, data: UserWithPassword, org_slug: str):
|
||||
user = await install_create_organization_user(request, data, org_slug)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
@router.post("/sample")
|
||||
async def api_install_user_sample(request: Request, username: str, org_slug: str):
|
||||
sample = await create_sample_data(org_slug, username, request)
|
||||
|
||||
return sample
|
||||
|
||||
|
||||
@router.post("/update")
|
||||
async def api_update_install_instance(request: Request, data: dict, step: int):
|
||||
request.app.db["installs"]
|
||||
|
||||
# get latest created install
|
||||
install = await update_install_instance(request, data, step)
|
||||
|
||||
return install
|
||||
63
apps/api/src/routers/orgs.py
Normal file
63
apps/api/src/routers/orgs.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
|
||||
from fastapi import APIRouter, Depends, Request, UploadFile
|
||||
from src.security.auth import get_current_user
|
||||
from src.services.orgs.orgs import Organization, create_org, delete_org, get_organization, get_organization_by_slug, get_orgs_by_user, update_org, update_org_logo
|
||||
from src.services.users.users import PublicUser, User
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def api_create_org(request: Request, org_object: Organization, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Create new organization
|
||||
"""
|
||||
return await create_org(request, org_object, current_user)
|
||||
|
||||
|
||||
@router.get("/{org_id}")
|
||||
async def api_get_org(request: Request, org_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get single Org by ID
|
||||
"""
|
||||
return await get_organization(request, org_id)
|
||||
|
||||
|
||||
@router.get("/slug/{org_slug}")
|
||||
async def api_get_org_by_slug(request: Request, org_slug: str, current_user: User = Depends(get_current_user)):
|
||||
"""
|
||||
Get single Org by Slug
|
||||
"""
|
||||
return await get_organization_by_slug(request, org_slug)
|
||||
|
||||
@router.put("/{org_id}/logo")
|
||||
async def api_update_org_logo(request: Request, org_id: str, logo_file:UploadFile, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get single Org by Slug
|
||||
"""
|
||||
return await update_org_logo(request=request,logo_file=logo_file, org_id=org_id, current_user=current_user)
|
||||
|
||||
@router.get("/user/page/{page}/limit/{limit}")
|
||||
async def api_user_orgs(request: Request, page: int, limit: int, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get orgs by page and limit by user
|
||||
"""
|
||||
return await get_orgs_by_user(request, current_user.user_id, page, limit)
|
||||
|
||||
|
||||
@router.put("/{org_id}")
|
||||
async def api_update_org(request: Request, org_object: Organization, org_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Update Org by ID
|
||||
"""
|
||||
return await update_org(request, org_object, org_id, current_user)
|
||||
|
||||
|
||||
@router.delete("/{org_id}")
|
||||
async def api_delete_org(request: Request, org_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Delete Org by ID
|
||||
"""
|
||||
|
||||
return await delete_org(request, org_id, current_user)
|
||||
41
apps/api/src/routers/roles.py
Normal file
41
apps/api/src/routers/roles.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
from fastapi import APIRouter, Depends, Request
|
||||
from src.security.auth import get_current_user
|
||||
from src.services.roles.schemas.roles import Role
|
||||
from src.services.roles.roles import create_role, delete_role, read_role, update_role
|
||||
from src.services.users.schemas.users import PublicUser
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def api_create_role(request: Request, role_object: Role, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Create new role
|
||||
"""
|
||||
return await create_role(request, role_object, current_user)
|
||||
|
||||
|
||||
@router.get("/{role_id}")
|
||||
async def api_get_role(request: Request, role_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get single role by role_id
|
||||
"""
|
||||
return await read_role(request, role_id, current_user)
|
||||
|
||||
|
||||
@router.put("/{role_id}")
|
||||
async def api_update_role(request: Request, role_object: Role, role_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Update role by role_id
|
||||
"""
|
||||
return await update_role(request, role_id, role_object, current_user)
|
||||
|
||||
|
||||
@router.delete("/{role_id}")
|
||||
async def api_delete_role(request: Request, role_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Delete role by ID
|
||||
"""
|
||||
|
||||
return await delete_role(request, role_id, current_user)
|
||||
56
apps/api/src/routers/trail.py
Normal file
56
apps/api/src/routers/trail.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
from fastapi import APIRouter, Depends, Request
|
||||
from src.security.auth import get_current_user
|
||||
from src.services.trail.trail import Trail, add_activity_to_trail, add_course_to_trail, create_trail, get_user_trail_with_orgslug, get_user_trail, remove_course_from_trail
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/start")
|
||||
async def api_start_trail(request: Request, trail_object: Trail, org_id: str, user=Depends(get_current_user)) -> Trail:
|
||||
"""
|
||||
Start trail
|
||||
"""
|
||||
return await create_trail(request, user, org_id, trail_object)
|
||||
|
||||
|
||||
@router.get("/org_id/{org_id}/trail")
|
||||
async def api_get_trail_by_orgid(request: Request, org_slug: str, user=Depends(get_current_user)):
|
||||
"""
|
||||
Get a user trails
|
||||
"""
|
||||
return await get_user_trail(request, user=user, org_slug=org_slug)
|
||||
|
||||
|
||||
@router.get("/org_slug/{org_slug}/trail")
|
||||
async def api_get_trail_by_orgslug(request: Request, org_slug: str, user=Depends(get_current_user)):
|
||||
"""
|
||||
Get a user trails using org slug
|
||||
"""
|
||||
return await get_user_trail_with_orgslug(request, user, org_slug=org_slug)
|
||||
|
||||
# Courses in trail
|
||||
|
||||
|
||||
@router.post("/org_slug/{org_slug}/add_course/{course_id}")
|
||||
async def api_add_course_to_trail(request: Request, course_id: str, org_slug: str, user=Depends(get_current_user)):
|
||||
"""
|
||||
Add Course to trail
|
||||
"""
|
||||
return await add_course_to_trail(request, user, org_slug, course_id)
|
||||
|
||||
|
||||
@router.post("/org_slug/{org_slug}/remove_course/{course_id}")
|
||||
async def api_remove_course_to_trail(request: Request, course_id: str, org_slug: str, user=Depends(get_current_user)):
|
||||
"""
|
||||
Remove Course from trail
|
||||
"""
|
||||
return await remove_course_from_trail(request, user, org_slug, course_id)
|
||||
|
||||
|
||||
@router.post("/org_slug/{org_slug}/add_activity/course_id/{course_id}/activity_id/{activity_id}")
|
||||
async def api_add_activity_to_trail(request: Request, activity_id: str, course_id: str, org_slug: str, user=Depends(get_current_user)):
|
||||
"""
|
||||
Add Course to trail
|
||||
"""
|
||||
return await add_activity_to_trail(request, user, course_id, org_slug, activity_id)
|
||||
66
apps/api/src/routers/users.py
Normal file
66
apps/api/src/routers/users.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
from fastapi import Depends, APIRouter, Request
|
||||
from src.security.auth import get_current_user
|
||||
from src.services.users.schemas.users import PasswordChangeForm, PublicUser, User, UserWithPassword
|
||||
from src.services.users.users import create_user, delete_user, get_profile_metadata, get_user_by_userid, update_user, update_user_password
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/profile")
|
||||
async def api_get_current_user(current_user: User = Depends(get_current_user)):
|
||||
"""
|
||||
Get current user
|
||||
"""
|
||||
return current_user.dict()
|
||||
|
||||
@router.get("/profile_metadata")
|
||||
async def api_get_current_user_metadata(request: Request,current_user: User = Depends(get_current_user)):
|
||||
"""
|
||||
Get current user
|
||||
"""
|
||||
return await get_profile_metadata(request , current_user.dict())
|
||||
|
||||
|
||||
|
||||
@router.get("/user_id/{user_id}")
|
||||
async def api_get_user_by_userid(request: Request,user_id: str):
|
||||
"""
|
||||
Get single user by user_id
|
||||
"""
|
||||
return await get_user_by_userid(request, user_id)
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def api_create_user(request: Request,user_object: UserWithPassword, org_slug: str ):
|
||||
"""
|
||||
Create new user
|
||||
"""
|
||||
return await create_user(request, None, user_object, org_slug)
|
||||
|
||||
|
||||
@router.delete("/user_id/{user_id}")
|
||||
async def api_delete_user(request: Request, user_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Delete user by ID
|
||||
"""
|
||||
|
||||
return await delete_user(request, current_user, user_id)
|
||||
|
||||
|
||||
@router.put("/user_id/{user_id}")
|
||||
async def api_update_user(request: Request, user_object: User, user_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Update user by ID
|
||||
"""
|
||||
return await update_user(request, user_id, user_object, current_user)
|
||||
|
||||
@router.put("/password/user_id/{user_id}")
|
||||
async def api_update_user_password(request: Request, user_id: str , passwordChangeForm : PasswordChangeForm, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Update user password by ID
|
||||
"""
|
||||
return await update_user_password(request,current_user, user_id, passwordChangeForm)
|
||||
0
apps/api/src/security/__init__.py
Normal file
0
apps/api/src/security/__init__.py
Normal file
94
apps/api/src/security/auth.py
Normal file
94
apps/api/src/security/auth.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
from config.config import get_learnhouse_config
|
||||
from pydantic import BaseModel
|
||||
from fastapi import Depends, HTTPException, Request, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jose import JWTError, jwt
|
||||
from datetime import datetime, timedelta
|
||||
from src.services.dev.dev import isDevModeEnabled
|
||||
from src.services.users.schemas.users import AnonymousUser, PublicUser
|
||||
from src.services.users.users import security_get_user, security_verify_password
|
||||
from src.security.security import ALGORITHM, SECRET_KEY
|
||||
from fastapi_jwt_auth import AuthJWT
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
|
||||
|
||||
|
||||
#### JWT Auth ####################################################
|
||||
class Settings(BaseModel):
|
||||
authjwt_secret_key: str = "secret" if isDevModeEnabled() else SECRET_KEY
|
||||
authjwt_token_location = {"cookies", "headers"}
|
||||
authjwt_cookie_csrf_protect = False
|
||||
authjwt_access_token_expires = False if isDevModeEnabled() else 28800
|
||||
authjwt_cookie_samesite = "lax"
|
||||
authjwt_cookie_secure = True
|
||||
authjwt_cookie_domain = get_learnhouse_config().hosting_config.cookie_config.domain
|
||||
|
||||
|
||||
@AuthJWT.load_config # type: ignore
|
||||
def get_config():
|
||||
return Settings()
|
||||
|
||||
|
||||
#### JWT Auth ####################################################
|
||||
|
||||
|
||||
#### Classes ####################################################
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
|
||||
class TokenData(BaseModel):
|
||||
username: str | None = None
|
||||
|
||||
|
||||
#### Classes ####################################################
|
||||
|
||||
|
||||
async def authenticate_user(request: Request, email: str, password: str):
|
||||
user = await security_get_user(request, email)
|
||||
if not user:
|
||||
return False
|
||||
if not await security_verify_password(password, user.password):
|
||||
return False
|
||||
return user
|
||||
|
||||
|
||||
def create_access_token(data: dict, expires_delta: timedelta | None = None):
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=15)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
async def get_current_user(request: Request, Authorize: AuthJWT = Depends()):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
try:
|
||||
Authorize.jwt_optional()
|
||||
username = Authorize.get_jwt_subject() or None
|
||||
token_data = TokenData(username=username) # type: ignore
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
if username:
|
||||
user = await security_get_user(request, email=token_data.username) # type: ignore # treated as an email
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return PublicUser(**user.dict())
|
||||
else:
|
||||
return AnonymousUser()
|
||||
|
||||
|
||||
async def non_public_endpoint(current_user: PublicUser):
|
||||
if isinstance(current_user, AnonymousUser):
|
||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||
149
apps/api/src/security/rbac/rbac.py
Normal file
149
apps/api/src/security/rbac/rbac.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
from typing import Literal
|
||||
from fastapi import HTTPException, status, Request
|
||||
from src.security.rbac.utils import check_element_type, get_id_identifier_of_element
|
||||
from src.services.roles.schemas.roles import RoleInDB
|
||||
from src.services.users.schemas.users import UserRolesInOrganization
|
||||
|
||||
|
||||
async def authorization_verify_if_element_is_public(
|
||||
request,
|
||||
element_id: str,
|
||||
user_id: str,
|
||||
action: Literal["read"],
|
||||
):
|
||||
element_nature = await check_element_type(element_id)
|
||||
|
||||
# Verifies if the element is public
|
||||
if (
|
||||
element_nature == ("courses" or "collections")
|
||||
and action == "read"
|
||||
and user_id == "anonymous"
|
||||
):
|
||||
if element_nature == "courses":
|
||||
courses = request.app.db["courses"]
|
||||
course = await courses.find_one({"course_id": element_id})
|
||||
|
||||
if course["public"]:
|
||||
return True
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User rights (public content) : You don't have the right to perform this action",
|
||||
)
|
||||
|
||||
if element_nature == "collections":
|
||||
collections = request.app.db["collections"]
|
||||
collection = await collections.find_one({"collection_id": element_id})
|
||||
|
||||
if collection["public"]:
|
||||
return True
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User rights (public content) : You don't have the right to perform this action",
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User rights (public content) : You don't have the right to perform this action",
|
||||
)
|
||||
|
||||
|
||||
async def authorization_verify_if_user_is_author(
|
||||
request,
|
||||
user_id: str,
|
||||
action: Literal["read", "update", "delete", "create"],
|
||||
element_id: str,
|
||||
):
|
||||
if action == "update" or "delete" or "read":
|
||||
element_nature = await check_element_type(element_id)
|
||||
elements = request.app.db[element_nature]
|
||||
element_identifier = await get_id_identifier_of_element(element_id)
|
||||
element = await elements.find_one({element_identifier: element_id})
|
||||
if user_id in element["authors"]:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
async def authorization_verify_based_on_roles(
|
||||
request: Request,
|
||||
user_id: str,
|
||||
action: Literal["read", "update", "delete", "create"],
|
||||
roles_list: list[UserRolesInOrganization],
|
||||
element_id: str,
|
||||
):
|
||||
element_type = await check_element_type(element_id)
|
||||
element = request.app.db[element_type]
|
||||
roles = request.app.db["roles"]
|
||||
|
||||
# Get the element
|
||||
element_identifier = await get_id_identifier_of_element(element_id)
|
||||
element = await element.find_one({element_identifier: element_id})
|
||||
|
||||
# Get the roles of the user
|
||||
roles_id_list = [role["role_id"] for role in roles_list]
|
||||
roles = await roles.find({"role_id": {"$in": roles_id_list}}).to_list(length=100)
|
||||
|
||||
async def checkRoles():
|
||||
# Check Roles
|
||||
for role in roles:
|
||||
role = RoleInDB(**role)
|
||||
if role.elements[element_type][f"action_{action}"] is True:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
async def checkOrgRoles():
|
||||
# Check Org Roles
|
||||
users = request.app.db["users"]
|
||||
user = await users.find_one({"user_id": user_id})
|
||||
if element is not None:
|
||||
for org in user["orgs"]:
|
||||
if org["org_id"] == element["org_id"]:
|
||||
if org["org_role"] == "owner" or org["org_role"] == "editor":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
if await checkRoles() or await checkOrgRoles():
|
||||
return True
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User rights (roless) : You don't have the right to perform this action",
|
||||
)
|
||||
|
||||
|
||||
async def authorization_verify_based_on_roles_and_authorship(
|
||||
request: Request,
|
||||
user_id: str,
|
||||
action: Literal["read", "update", "delete", "create"],
|
||||
roles_list: list[UserRolesInOrganization],
|
||||
element_id: str,
|
||||
):
|
||||
isAuthor = await authorization_verify_if_user_is_author(
|
||||
request, user_id, action, element_id
|
||||
)
|
||||
|
||||
isRole = await authorization_verify_based_on_roles(
|
||||
request, user_id, action, roles_list, element_id
|
||||
)
|
||||
|
||||
if isAuthor or isRole:
|
||||
return True
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User rights (roles & authorship) : You don't have the right to perform this action",
|
||||
)
|
||||
|
||||
|
||||
async def authorization_verify_if_user_is_anon(user_id: str):
|
||||
if user_id == "anonymous":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="You should be logged in to perform this action",
|
||||
)
|
||||
45
apps/api/src/security/rbac/utils.py
Normal file
45
apps/api/src/security/rbac/utils.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
from fastapi import HTTPException, status
|
||||
|
||||
|
||||
async def check_element_type(element_id):
|
||||
"""
|
||||
Check if the element is a course, a user, a house or a collection, by checking its prefix
|
||||
"""
|
||||
if element_id.startswith("course_"):
|
||||
return "courses"
|
||||
elif element_id.startswith("user_"):
|
||||
return "users"
|
||||
elif element_id.startswith("house_"):
|
||||
return "houses"
|
||||
elif element_id.startswith("org_"):
|
||||
return "organizations"
|
||||
elif element_id.startswith("coursechapter_"):
|
||||
return "coursechapters"
|
||||
elif element_id.startswith("collection_"):
|
||||
return "collections"
|
||||
elif element_id.startswith("activity_"):
|
||||
return "activities"
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="User rights : Issue verifying element nature",
|
||||
)
|
||||
|
||||
|
||||
async def get_singular_form_of_element(element_id):
|
||||
element_type = await check_element_type(element_id)
|
||||
|
||||
if element_type == "activities":
|
||||
return "activity"
|
||||
else:
|
||||
singular_form_element = element_type[:-1]
|
||||
return singular_form_element
|
||||
|
||||
|
||||
async def get_id_identifier_of_element(element_id):
|
||||
singular_form_element = await get_singular_form_of_element(element_id)
|
||||
|
||||
if singular_form_element == "ogranizations":
|
||||
return "org_id"
|
||||
else:
|
||||
return str(singular_form_element) + "_id"
|
||||
30
apps/api/src/security/security.py
Normal file
30
apps/api/src/security/security.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
from passlib.context import CryptContext
|
||||
from passlib.hash import pbkdf2_sha256
|
||||
from config.config import get_learnhouse_config
|
||||
|
||||
|
||||
### 🔒 JWT ##############################################################
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
||||
SECRET_KEY = get_learnhouse_config().security_config.auth_jwt_secret_key
|
||||
ALGORITHM = "HS256"
|
||||
|
||||
### 🔒 JWT ##############################################################
|
||||
|
||||
|
||||
### 🔒 Passwords Hashing ##############################################################
|
||||
|
||||
|
||||
async def security_hash_password(password: str):
|
||||
return pbkdf2_sha256.hash(password)
|
||||
|
||||
|
||||
async def security_verify_password(plain_password: str, hashed_password: str):
|
||||
return pbkdf2_sha256.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
### 🔒 Passwords Hashing ##############################################################
|
||||
|
||||
|
||||
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