feat: refactor the entire learnhouse project

This commit is contained in:
swve 2023-10-13 20:03:27 +02:00
parent f556e41dda
commit 4c215e91d5
247 changed files with 7716 additions and 1013 deletions

171
apps/api/.gitignore vendored Normal file
View file

@ -0,0 +1,171 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Visual Studio Code
.vscode/
# Learnhouse
content/org_*
# Flyio
fly.toml
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# ruff
.ruff/
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

17
apps/api/Dockerfile Normal file
View file

@ -0,0 +1,17 @@
#
FROM python:3.11
#
WORKDIR /usr/learnhouse/apps/api
#
COPY ./requirements.txt /usr/learnhouse/requirements.txt
#
RUN pip install --no-cache-dir --upgrade -r /usr/learnhouse/requirements.txt
#
COPY ./ /usr/learnhouse
#
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80" , "--reload" ]

0
apps/api/__init__.py Normal file
View file

68
apps/api/app.py Normal file
View file

@ -0,0 +1,68 @@
from fastapi import FastAPI, Request
from config.config import LearnHouseConfig, get_learnhouse_config
from src.core.events.events import shutdown_app, startup_app
from src.router import v1_router
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi_jwt_auth.exceptions import AuthJWTException
from fastapi.middleware.gzip import GZipMiddleware
# from src.services.mocks.initial import create_initial_data
########################
# Pre-Alpha Version 0.1.0
# Author: @swve
# (c) LearnHouse 2022
########################
# Get LearnHouse Config
learnhouse_config: LearnHouseConfig = get_learnhouse_config()
# Global Config
app = FastAPI(
title=learnhouse_config.site_name,
description=learnhouse_config.site_description,
version="0.1.0",
root_path="/",
)
app.add_middleware(
CORSMiddleware,
allow_origin_regex=learnhouse_config.hosting_config.allowed_regexp,
allow_methods=["*"],
allow_credentials=True,
allow_headers=["*"],
)
# Gzip Middleware (will add brotli later)
app.add_middleware(GZipMiddleware, minimum_size=1000)
# Events
app.add_event_handler("startup", startup_app(app))
app.add_event_handler("shutdown", shutdown_app(app))
# JWT Exception Handler
@app.exception_handler(AuthJWTException)
def authjwt_exception_handler(request: Request, exc: AuthJWTException):
return JSONResponse(
status_code=exc.status_code, # type: ignore
content={"detail": exc.message}, # type: ignore
)
# Static Files
app.mount("/content", StaticFiles(directory="content"), name="content")
# Global Routes
app.include_router(v1_router)
# General Routes
@app.get("/")
async def root():
return {"Message": "Welcome to LearnHouse ✨"}

View file

229
apps/api/config/config.py Normal file
View file

@ -0,0 +1,229 @@
from typing import Literal, Optional
from pydantic import BaseModel
import os
import yaml
class SentryConfig(BaseModel):
dsn: str
environment: str
release: str
class CookieConfig(BaseModel):
domain: str
class GeneralConfig(BaseModel):
development_mode: bool
install_mode: bool
class SecurityConfig(BaseModel):
auth_jwt_secret_key: str
class S3ApiConfig(BaseModel):
bucket_name: str | None
endpoint_url: str | None
class ContentDeliveryConfig(BaseModel):
type: Literal["filesystem", "s3api"]
s3api: S3ApiConfig
class HostingConfig(BaseModel):
domain: str
ssl: bool
use_default_org: bool
allowed_origins: list
allowed_regexp: str
self_hosted: bool
sentry_config: Optional[SentryConfig]
cookie_config: CookieConfig
content_delivery: ContentDeliveryConfig
class DatabaseConfig(BaseModel):
mongodb_connection_string: Optional[str]
class LearnHouseConfig(BaseModel):
site_name: str
site_description: str
contact_email: str
general_config: GeneralConfig
hosting_config: HostingConfig
database_config: DatabaseConfig
security_config: SecurityConfig
def get_learnhouse_config() -> LearnHouseConfig:
# Get the YAML file
yaml_path = os.path.join(os.path.dirname(__file__), "config.yaml")
# Load the YAML file
with open(yaml_path, "r") as f:
yaml_config = yaml.safe_load(f)
# General Config
# Development Mode & Install Mode
env_development_mode = eval(os.environ.get("LEARNHOUSE_DEVELOPMENT_MODE", "None"))
development_mode = (
env_development_mode
if env_development_mode is not None
else yaml_config.get("general", {}).get("development_mode")
)
env_install_mode = os.environ.get("LEARNHOUSE_INSTALL_MODE", "None")
install_mode = (
env_install_mode
if env_install_mode is not None
else yaml_config.get("general", {}).get("install_mode")
)
# Security Config
env_auth_jwt_secret_key = os.environ.get("LEARNHOUSE_AUTH_JWT_SECRET_KEY")
auth_jwt_secret_key = env_auth_jwt_secret_key or yaml_config.get(
"security", {}
).get("auth_jwt_secret_key")
# Check if environment variables are defined
env_site_name = os.environ.get("LEARNHOUSE_SITE_NAME")
env_site_description = os.environ.get("LEARNHOUSE_SITE_DESCRIPTION")
env_contact_email = os.environ.get("LEARNHOUSE_CONTACT_EMAIL")
env_domain = os.environ.get("LEARNHOUSE_DOMAIN")
os.environ.get("LEARNHOUSE_PORT")
env_ssl = os.environ.get("LEARNHOUSE_SSL")
env_use_default_org = os.environ.get("LEARNHOUSE_USE_DEFAULT_ORG")
env_allowed_origins = os.environ.get("LEARNHOUSE_ALLOWED_ORIGINS")
env_cookie_domain = os.environ.get("LEARNHOUSE_COOKIE_DOMAIN")
# Allowed origins should be a comma separated string
if env_allowed_origins:
env_allowed_origins = env_allowed_origins.split(",")
env_allowed_regexp = os.environ.get("LEARNHOUSE_ALLOWED_REGEXP")
env_self_hosted = os.environ.get("LEARNHOUSE_SELF_HOSTED")
env_mongodb_connection_string = os.environ.get(
"LEARNHOUSE_MONGODB_CONNECTION_STRING"
)
# Sentry Config
env_sentry_dsn = os.environ.get("LEARNHOUSE_SENTRY_DSN")
env_sentry_environment = os.environ.get("LEARNHOUSE_SENTRY_ENVIRONMENT")
env_sentry_release = os.environ.get("LEARNHOUSE_SENTRY_RELEASE")
# Fill in values with YAML file if they are not provided
site_name = env_site_name or yaml_config.get("site_name")
site_description = env_site_description or yaml_config.get("site_description")
contact_email = env_contact_email or yaml_config.get("contact_email")
domain = env_domain or yaml_config.get("hosting_config", {}).get("domain")
ssl = env_ssl or yaml_config.get("hosting_config", {}).get("ssl")
use_default_org = env_use_default_org or yaml_config.get("hosting_config", {}).get(
"use_default_org"
)
allowed_origins = env_allowed_origins or yaml_config.get("hosting_config", {}).get(
"allowed_origins"
)
allowed_regexp = env_allowed_regexp or yaml_config.get("hosting_config", {}).get(
"allowed_regexp"
)
self_hosted = env_self_hosted or yaml_config.get("hosting_config", {}).get(
"self_hosted"
)
cookies_domain = env_cookie_domain or yaml_config.get("hosting_config", {}).get(
"cookies_config", {}
).get("domain")
cookie_config = CookieConfig(domain=cookies_domain)
env_content_delivery_type = os.environ.get("LEARNHOUSE_CONTENT_DELIVERY_TYPE")
content_delivery_type: str = env_content_delivery_type or (
(yaml_config.get("hosting_config", {}).get("content_delivery", {}).get("type"))
or "filesystem"
) # default to filesystem
env_bucket_name = os.environ.get("LEARNHOUSE_S3_API_BUCKET_NAME")
env_endpoint_url = os.environ.get("LEARNHOUSE_S3_API_ENDPOINT_URL")
bucket_name = (
yaml_config.get("hosting_config", {})
.get("content_delivery", {})
.get("s3api", {})
.get("bucket_name")
) or env_bucket_name
endpoint_url = (
yaml_config.get("hosting_config", {})
.get("content_delivery", {})
.get("s3api", {})
.get("endpoint_url")
) or env_endpoint_url
content_delivery = ContentDeliveryConfig(
type=content_delivery_type, # type: ignore
s3api=S3ApiConfig(bucket_name=bucket_name, endpoint_url=endpoint_url), # type: ignore
)
# Database config
mongodb_connection_string = env_mongodb_connection_string or yaml_config.get(
"database_config", {}
).get("mongodb_connection_string")
# Sentry config
# check if the sentry config is provided in the YAML file
sentry_config_verif = (
yaml_config.get("hosting_config", {}).get("sentry_config")
or env_sentry_dsn
or env_sentry_environment
or env_sentry_release
or None
)
sentry_dsn = env_sentry_dsn or yaml_config.get("hosting_config", {}).get(
"sentry_config", {}
).get("dsn")
sentry_environment = env_sentry_environment or yaml_config.get(
"hosting_config", {}
).get("sentry_config", {}).get("environment")
sentry_release = env_sentry_release or yaml_config.get("hosting_config", {}).get(
"sentry_config", {}
).get("release")
if sentry_config_verif:
sentry_config = SentryConfig(
dsn=sentry_dsn, environment=sentry_environment, release=sentry_release
)
else:
sentry_config = None
# Create HostingConfig and DatabaseConfig objects
hosting_config = HostingConfig(
domain=domain,
ssl=bool(ssl),
use_default_org=bool(use_default_org),
allowed_origins=list(allowed_origins),
allowed_regexp=allowed_regexp,
self_hosted=bool(self_hosted),
sentry_config=sentry_config,
cookie_config=cookie_config,
content_delivery=content_delivery,
)
database_config = DatabaseConfig(
mongodb_connection_string=mongodb_connection_string
)
# Create LearnHouseConfig object
config = LearnHouseConfig(
site_name=site_name,
site_description=site_description,
contact_email=contact_email,
general_config=GeneralConfig(
development_mode=bool(development_mode), install_mode=bool(install_mode)
),
hosting_config=hosting_config,
database_config=database_config,
security_config=SecurityConfig(auth_jwt_secret_key=auth_jwt_secret_key),
)
return config

View file

@ -0,0 +1,28 @@
site_name: LearnHouse
site_description: LearnHouse is an open-source platform tailored for learning experiences.
contact_email: hi@learnhouse.app
general:
development_mode: true
install_mode: true
security:
auth_jwt_secret_key: secret
hosting_config:
domain: learnhouse.app
ssl: true
allowed_origins:
- http://localhost:3000
- http://localhost:3001
cookies_config:
domain: ".localhost"
allowed_regexp: '\b((?:https?://)[^\s/$.?#].[^\s]*)\b'
content_delivery:
type: "filesystem" # "filesystem" or "s3api"
s3api:
bucket_name: ""
endpoint_url: ""
database_config:
mongodb_connection_string: mongodb://learnhouse:learnhouse@mongo:27017/

View file

3
apps/api/pyproject.toml Normal file
View file

@ -0,0 +1,3 @@
[tool.ruff]
# E501 line too long (82 > 79 characters)
ignore = ["E501"]

15
apps/api/requirements.txt Normal file
View file

@ -0,0 +1,15 @@
fastapi==0.101.1
pydantic>=1.8.0,<2.0.0
uvicorn==0.23.2
pymongo==4.3.3
motor==3.1.1
python-multipart
boto3
botocore
python-jose
passlib
fastapi-jwt-auth
faker
requests
pyyaml
sentry-sdk[fastapi]

0
apps/api/src/__init__.py Normal file
View file

View file

View file

View 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")

View 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

View 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

View 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")

View 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
View 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)],
)

View file

View 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"}

View 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)

View 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
)

View 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)

View 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)

View 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)

View 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 🤖"}

View file

View 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

View 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)

View 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)

View 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)

View 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)

View file

View 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")

View 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",
)

View 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"

View 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 ##############################################################

View file

View file

View 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"
)

View file

@ -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"
)

View file

@ -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

View file

@ -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"
)

View 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

View 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

View 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

View 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 ####################################################

View 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

View 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"}

View 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"}

View 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

View 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 ####################################################

View 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 ####################################################

View 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 ####################################################

View 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"}

View file

View 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")

View 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)

View 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,
)

View file

View 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

View 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 ####################################################

View 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)

View file

View 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 ####################################################

View 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

View file

View 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)

View file

View 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

View 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
)

View 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)}")