mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: refactor the entire learnhouse project
This commit is contained in:
parent
f556e41dda
commit
4c215e91d5
247 changed files with 7716 additions and 1013 deletions
171
apps/api/.gitignore
vendored
Normal file
171
apps/api/.gitignore
vendored
Normal 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
17
apps/api/Dockerfile
Normal 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
0
apps/api/__init__.py
Normal file
68
apps/api/app.py
Normal file
68
apps/api/app.py
Normal 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 ✨"}
|
||||
|
||||
0
apps/api/config/__init__.py
Normal file
0
apps/api/config/__init__.py
Normal file
229
apps/api/config/config.py
Normal file
229
apps/api/config/config.py
Normal 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
|
||||
28
apps/api/config/config.yaml
Normal file
28
apps/api/config/config.yaml
Normal 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/
|
||||
0
apps/api/content/__init__.py
Normal file
0
apps/api/content/__init__.py
Normal file
3
apps/api/pyproject.toml
Normal file
3
apps/api/pyproject.toml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[tool.ruff]
|
||||
# E501 line too long (82 > 79 characters)
|
||||
ignore = ["E501"]
|
||||
15
apps/api/requirements.txt
Normal file
15
apps/api/requirements.txt
Normal 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
0
apps/api/src/__init__.py
Normal file
0
apps/api/src/core/__init__.py
Normal file
0
apps/api/src/core/__init__.py
Normal file
0
apps/api/src/core/events/__init__.py
Normal file
0
apps/api/src/core/events/__init__.py
Normal file
8
apps/api/src/core/events/content.py
Normal file
8
apps/api/src/core/events/content.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import os
|
||||
|
||||
|
||||
async def check_content_directory():
|
||||
if not os.path.exists("content"):
|
||||
# create folder for activity
|
||||
print("Creating content directory...")
|
||||
os.makedirs("content")
|
||||
21
apps/api/src/core/events/database.py
Normal file
21
apps/api/src/core/events/database.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import logging
|
||||
from fastapi import FastAPI
|
||||
import motor.motor_asyncio
|
||||
|
||||
|
||||
async def connect_to_db(app: FastAPI):
|
||||
logging.info("Connecting to database...")
|
||||
try:
|
||||
app.mongodb_client = motor.motor_asyncio.AsyncIOMotorClient( # type: ignore
|
||||
app.learnhouse_config.database_config.mongodb_connection_string) # type: ignore
|
||||
app.db = app.mongodb_client["learnhouse"] # type: ignore
|
||||
logging.info("Connected to database!")
|
||||
except Exception as e:
|
||||
logging.error("Failed to connect to database!")
|
||||
logging.error(e)
|
||||
|
||||
|
||||
async def close_database(app: FastAPI):
|
||||
app.mongodb_client.close() # type: ignore
|
||||
logging.info("LearnHouse has been shut down.")
|
||||
return app
|
||||
35
apps/api/src/core/events/events.py
Normal file
35
apps/api/src/core/events/events.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
from typing import Callable
|
||||
from fastapi import FastAPI
|
||||
from config.config import LearnHouseConfig, get_learnhouse_config
|
||||
from src.core.events.content import check_content_directory
|
||||
from src.core.events.database import close_database, connect_to_db
|
||||
from src.core.events.logs import create_logs_dir
|
||||
from src.core.events.sentry import init_sentry
|
||||
|
||||
|
||||
def startup_app(app: FastAPI) -> Callable:
|
||||
async def start_app() -> None:
|
||||
# Get LearnHouse Config
|
||||
learnhouse_config: LearnHouseConfig = get_learnhouse_config()
|
||||
app.learnhouse_config = learnhouse_config # type: ignore
|
||||
|
||||
# Init Sentry
|
||||
await init_sentry(app)
|
||||
|
||||
# Connect to database
|
||||
await connect_to_db(app)
|
||||
|
||||
# Create logs directory
|
||||
await create_logs_dir()
|
||||
|
||||
# Create content directory
|
||||
await check_content_directory()
|
||||
|
||||
return start_app
|
||||
|
||||
|
||||
def shutdown_app(app: FastAPI) -> Callable:
|
||||
async def close_app() -> None:
|
||||
await close_database(app)
|
||||
|
||||
return close_app
|
||||
24
apps/api/src/core/events/logs.py
Normal file
24
apps/api/src/core/events/logs.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import logging
|
||||
import os
|
||||
|
||||
|
||||
async def create_logs_dir():
|
||||
if not os.path.exists("logs"):
|
||||
os.mkdir("logs")
|
||||
|
||||
# Initiate logging
|
||||
async def init_logging():
|
||||
await create_logs_dir()
|
||||
|
||||
# Logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
datefmt="%d-%b-%y %H:%M:%S",
|
||||
handlers=[
|
||||
logging.FileHandler("logs/learnhouse.log"),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
|
||||
logging.info("Logging initiated")
|
||||
16
apps/api/src/core/events/sentry.py
Normal file
16
apps/api/src/core/events/sentry.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from fastapi import FastAPI
|
||||
|
||||
import sentry_sdk
|
||||
|
||||
from config.config import LearnHouseConfig
|
||||
|
||||
async def init_sentry(app: FastAPI) -> None:
|
||||
|
||||
leanrhouse_config : LearnHouseConfig = app.learnhouse_config # type: ignore
|
||||
if leanrhouse_config.hosting_config.sentry_config is not None:
|
||||
sentry_sdk.init(
|
||||
dsn=app.learnhouse_config.hosting_config.sentry_config.dsn, # type: ignore
|
||||
environment=app.learnhouse_config.hosting_config.sentry_config.environment, # type: ignore
|
||||
release=app.learnhouse_config.hosting_config.sentry_config.release, # type: ignore
|
||||
traces_sample_rate=1.0,
|
||||
)
|
||||
37
apps/api/src/router.py
Normal file
37
apps/api/src/router.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
from fastapi import APIRouter, Depends
|
||||
from src.routers import blocks, dev, trail, users, auth, orgs, roles
|
||||
from src.routers.courses import chapters, collections, courses, activities
|
||||
from src.routers.install import install
|
||||
from src.services.dev.dev import isDevModeEnabledOrRaise
|
||||
from src.services.install.install import isInstallModeEnabled
|
||||
|
||||
|
||||
v1_router = APIRouter(prefix="/api/v1")
|
||||
|
||||
|
||||
# API Routes
|
||||
v1_router.include_router(users.router, prefix="/users", tags=["users"])
|
||||
v1_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||
v1_router.include_router(orgs.router, prefix="/orgs", tags=["orgs"])
|
||||
v1_router.include_router(roles.router, prefix="/roles", tags=["roles"])
|
||||
v1_router.include_router(blocks.router, prefix="/blocks", tags=["blocks"])
|
||||
v1_router.include_router(courses.router, prefix="/courses", tags=["courses"])
|
||||
v1_router.include_router(chapters.router, prefix="/chapters", tags=["chapters"])
|
||||
v1_router.include_router(activities.router, prefix="/activities", tags=["activities"])
|
||||
v1_router.include_router(
|
||||
collections.router, prefix="/collections", tags=["collections"]
|
||||
)
|
||||
v1_router.include_router(trail.router, prefix="/trail", tags=["trail"])
|
||||
|
||||
# Dev Routes
|
||||
v1_router.include_router(
|
||||
dev.router, prefix="/dev", tags=["dev"], dependencies=[Depends(isDevModeEnabledOrRaise)]
|
||||
)
|
||||
|
||||
# Install Routes
|
||||
v1_router.include_router(
|
||||
install.router,
|
||||
prefix="/install",
|
||||
tags=["install"],
|
||||
dependencies=[Depends(isInstallModeEnabled)],
|
||||
)
|
||||
0
apps/api/src/routers/__init__.py
Normal file
0
apps/api/src/routers/__init__.py
Normal file
67
apps/api/src/routers/auth.py
Normal file
67
apps/api/src/routers/auth.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
from fastapi import Depends, APIRouter, HTTPException, Response, status, Request
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from config.config import get_learnhouse_config
|
||||
from src.security.auth import AuthJWT, authenticate_user
|
||||
from src.services.users.users import PublicUser
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/refresh")
|
||||
def refresh(response: Response,Authorize: AuthJWT = Depends()):
|
||||
"""
|
||||
The jwt_refresh_token_required() function insures a valid refresh
|
||||
token is present in the request before running any code below that function.
|
||||
we can use the get_jwt_subject() function to get the subject of the refresh
|
||||
token, and use the create_access_token() function again to make a new access token
|
||||
"""
|
||||
Authorize.jwt_refresh_token_required()
|
||||
|
||||
current_user = Authorize.get_jwt_subject()
|
||||
new_access_token = Authorize.create_access_token(subject=current_user) # type: ignore
|
||||
|
||||
response.set_cookie(key="access_token_cookie", value=new_access_token, httponly=False, domain=get_learnhouse_config().hosting_config.cookie_config.domain)
|
||||
return {"access_token": new_access_token}
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
async def login(
|
||||
request: Request,
|
||||
response: Response,
|
||||
Authorize: AuthJWT = Depends(),
|
||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||
):
|
||||
user = await authenticate_user(request, form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect Email or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
access_token = Authorize.create_access_token(subject=form_data.username)
|
||||
refresh_token = Authorize.create_refresh_token(subject=form_data.username)
|
||||
Authorize.set_refresh_cookies(refresh_token)
|
||||
# set cookies using fastapi
|
||||
response.set_cookie(key="access_token_cookie", value=access_token, httponly=False, domain=get_learnhouse_config().hosting_config.cookie_config.domain)
|
||||
user = PublicUser(**user.dict())
|
||||
|
||||
result = {
|
||||
"user": user,
|
||||
"tokens": {"access_token": access_token, "refresh_token": refresh_token},
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
@router.delete("/logout")
|
||||
def logout(Authorize: AuthJWT = Depends()):
|
||||
"""
|
||||
Because the JWT are stored in an httponly cookie now, we cannot
|
||||
log the user out by simply deleting the cookies in the frontend.
|
||||
We need the backend to send us a response to delete the cookies.
|
||||
"""
|
||||
Authorize.jwt_required()
|
||||
|
||||
Authorize.unset_jwt_cookies()
|
||||
return {"msg": "Successfully logout"}
|
||||
94
apps/api/src/routers/blocks.py
Normal file
94
apps/api/src/routers/blocks.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
from fastapi import APIRouter, Depends, UploadFile, Form, Request
|
||||
from src.security.auth import get_current_user
|
||||
from src.services.blocks.block_types.imageBlock.images import create_image_block, get_image_block
|
||||
from src.services.blocks.block_types.videoBlock.videoBlock import create_video_block, get_video_block
|
||||
from src.services.blocks.block_types.pdfBlock.pdfBlock import create_pdf_block, get_pdf_block
|
||||
from src.services.blocks.block_types.quizBlock.quizBlock import create_quiz_block, get_quiz_block_answers, get_quiz_block_options, quizBlock
|
||||
from src.services.users.users import PublicUser
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
####################
|
||||
# Image Block
|
||||
####################
|
||||
|
||||
@router.post("/image")
|
||||
async def api_create_image_file_block(request: Request, file_object: UploadFile, activity_id: str = Form(), current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Create new image file
|
||||
"""
|
||||
return await create_image_block(request, file_object, activity_id)
|
||||
|
||||
|
||||
@router.get("/image")
|
||||
async def api_get_image_file_block(request: Request, file_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get image file
|
||||
"""
|
||||
return await get_image_block(request, file_id, current_user)
|
||||
|
||||
####################
|
||||
# Video Block
|
||||
####################
|
||||
|
||||
@router.post("/video")
|
||||
async def api_create_video_file_block(request: Request, file_object: UploadFile, activity_id: str = Form(), current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Create new video file
|
||||
"""
|
||||
return await create_video_block(request, file_object, activity_id)
|
||||
|
||||
|
||||
@router.get("/video")
|
||||
async def api_get_video_file_block(request: Request, file_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get video file
|
||||
"""
|
||||
return await get_video_block(request, file_id, current_user)
|
||||
|
||||
####################
|
||||
# PDF Block
|
||||
####################
|
||||
|
||||
@router.post("/pdf")
|
||||
async def api_create_pdf_file_block(request: Request, file_object: UploadFile, activity_id: str = Form(), current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Create new pdf file
|
||||
"""
|
||||
return await create_pdf_block(request, file_object, activity_id)
|
||||
|
||||
|
||||
@router.get("/pdf")
|
||||
async def api_get_pdf_file_block(request: Request, file_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get pdf file
|
||||
"""
|
||||
return await get_pdf_block(request, file_id, current_user)
|
||||
|
||||
|
||||
####################
|
||||
# Quiz Block
|
||||
####################
|
||||
|
||||
@router.post("/quiz/{activity_id}")
|
||||
async def api_create_quiz_block(request: Request, quiz_block: quizBlock, activity_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Create new document file
|
||||
"""
|
||||
return await create_quiz_block(request, quiz_block, activity_id, current_user)
|
||||
|
||||
|
||||
@router.get("/quiz/options")
|
||||
async def api_get_quiz_options(request: Request, block_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get quiz options
|
||||
"""
|
||||
return await get_quiz_block_options(request, block_id, current_user)
|
||||
|
||||
|
||||
@router.get("/quiz/answers")
|
||||
async def api_get_quiz_answers(request: Request, block_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get quiz answers
|
||||
"""
|
||||
return await get_quiz_block_answers(request, block_id, current_user)
|
||||
131
apps/api/src/routers/courses/activities.py
Normal file
131
apps/api/src/routers/courses/activities.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
from fastapi import APIRouter, Depends, UploadFile, Form, Request
|
||||
from src.services.courses.activities.activities import (
|
||||
Activity,
|
||||
create_activity,
|
||||
get_activity,
|
||||
get_activities,
|
||||
update_activity,
|
||||
delete_activity,
|
||||
)
|
||||
from src.security.auth import get_current_user
|
||||
from src.services.courses.activities.pdf import create_documentpdf_activity
|
||||
from src.services.courses.activities.video import (
|
||||
ExternalVideo,
|
||||
create_external_video_activity,
|
||||
create_video_activity,
|
||||
)
|
||||
from src.services.users.schemas.users import PublicUser
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def api_create_activity(
|
||||
request: Request,
|
||||
activity_object: Activity,
|
||||
org_id: str,
|
||||
coursechapter_id: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Create new activity
|
||||
"""
|
||||
return await create_activity(
|
||||
request, activity_object, org_id, coursechapter_id, current_user
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{activity_id}")
|
||||
async def api_get_activity(
|
||||
request: Request,
|
||||
activity_id: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Get single activity by activity_id
|
||||
"""
|
||||
return await get_activity(request, activity_id, current_user=current_user)
|
||||
|
||||
|
||||
@router.get("/coursechapter/{coursechapter_id}")
|
||||
async def api_get_activities(
|
||||
request: Request,
|
||||
coursechapter_id: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Get CourseChapter activities
|
||||
"""
|
||||
return await get_activities(request, coursechapter_id, current_user)
|
||||
|
||||
|
||||
@router.put("/{activity_id}")
|
||||
async def api_update_activity(
|
||||
request: Request,
|
||||
activity_object: Activity,
|
||||
activity_id: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Update activity by activity_id
|
||||
"""
|
||||
return await update_activity(request, activity_object, activity_id, current_user)
|
||||
|
||||
|
||||
@router.delete("/{activity_id}")
|
||||
async def api_delete_activity(
|
||||
request: Request,
|
||||
activity_id: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Delete activity by activity_id
|
||||
"""
|
||||
return await delete_activity(request, activity_id, current_user)
|
||||
|
||||
|
||||
# Video activity
|
||||
|
||||
|
||||
@router.post("/video")
|
||||
async def api_create_video_activity(
|
||||
request: Request,
|
||||
name: str = Form(),
|
||||
coursechapter_id: str = Form(),
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
video_file: UploadFile | None = None,
|
||||
):
|
||||
"""
|
||||
Create new activity
|
||||
"""
|
||||
return await create_video_activity(
|
||||
request, name, coursechapter_id, current_user, video_file
|
||||
)
|
||||
|
||||
|
||||
@router.post("/external_video")
|
||||
async def api_create_external_video_activity(
|
||||
request: Request,
|
||||
external_video: ExternalVideo,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Create new activity
|
||||
"""
|
||||
return await create_external_video_activity(request, current_user, external_video)
|
||||
|
||||
|
||||
@router.post("/documentpdf")
|
||||
async def api_create_documentpdf_activity(
|
||||
request: Request,
|
||||
name: str = Form(),
|
||||
coursechapter_id: str = Form(),
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
pdf_file: UploadFile | None = None,
|
||||
):
|
||||
"""
|
||||
Create new activity
|
||||
"""
|
||||
return await create_documentpdf_activity(
|
||||
request, name, coursechapter_id, current_user, pdf_file
|
||||
)
|
||||
64
apps/api/src/routers/courses/chapters.py
Normal file
64
apps/api/src/routers/courses/chapters.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
from fastapi import APIRouter, Depends, Request
|
||||
|
||||
from src.services.courses.chapters import CourseChapter, CourseChapterMetaData, create_coursechapter, delete_coursechapter, get_coursechapter, get_coursechapters, get_coursechapters_meta, update_coursechapter, update_coursechapters_meta
|
||||
from src.services.users.users import PublicUser
|
||||
from src.security.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def api_create_coursechapter(request: Request,coursechapter_object: CourseChapter, course_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Create new CourseChapter
|
||||
"""
|
||||
return await create_coursechapter(request, coursechapter_object, course_id, current_user)
|
||||
|
||||
|
||||
@router.get("/{coursechapter_id}")
|
||||
async def api_get_coursechapter(request: Request,coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get single CourseChapter by coursechapter_id
|
||||
"""
|
||||
return await get_coursechapter(request, coursechapter_id, current_user=current_user)
|
||||
|
||||
|
||||
@router.get("/meta/{course_id}")
|
||||
async def api_get_coursechapter_meta(request: Request,course_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get coursechapter metadata
|
||||
"""
|
||||
return await get_coursechapters_meta(request, course_id, current_user=current_user)
|
||||
|
||||
|
||||
@router.put("/meta/{course_id}")
|
||||
async def api_update_coursechapter_meta(request: Request,course_id: str, coursechapters_metadata: CourseChapterMetaData, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Update coursechapter metadata
|
||||
"""
|
||||
return await update_coursechapters_meta(request, course_id, coursechapters_metadata, current_user=current_user)
|
||||
|
||||
|
||||
@router.get("/{course_id}/page/{page}/limit/{limit}")
|
||||
async def api_get_coursechapter_by(request: Request,course_id: str, page: int, limit: int):
|
||||
"""
|
||||
Get CourseChapters by page and limit
|
||||
"""
|
||||
return await get_coursechapters(request, course_id, page, limit)
|
||||
|
||||
|
||||
@router.put("/{coursechapter_id}")
|
||||
async def api_update_coursechapter(request: Request,coursechapter_object: CourseChapter, coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Update CourseChapters by course_id
|
||||
"""
|
||||
return await update_coursechapter(request, coursechapter_object, coursechapter_id, current_user)
|
||||
|
||||
|
||||
@router.delete("/{coursechapter_id}")
|
||||
async def api_delete_coursechapter(request: Request,coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Delete CourseChapters by ID
|
||||
"""
|
||||
|
||||
return await delete_coursechapter(request,coursechapter_id, current_user)
|
||||
80
apps/api/src/routers/courses/collections.py
Normal file
80
apps/api/src/routers/courses/collections.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
from fastapi import APIRouter, Depends, Request
|
||||
from src.security.auth import get_current_user
|
||||
from src.services.users.users import PublicUser
|
||||
from src.services.courses.collections import (
|
||||
Collection,
|
||||
create_collection,
|
||||
get_collection,
|
||||
get_collections,
|
||||
update_collection,
|
||||
delete_collection,
|
||||
)
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def api_create_collection(
|
||||
request: Request,
|
||||
collection_object: Collection,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Create new Collection
|
||||
"""
|
||||
return await create_collection(request, collection_object, current_user)
|
||||
|
||||
|
||||
@router.get("/{collection_id}")
|
||||
async def api_get_collection(
|
||||
request: Request,
|
||||
collection_id: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Get single collection by ID
|
||||
"""
|
||||
return await get_collection(request, collection_id, current_user)
|
||||
|
||||
|
||||
@router.get("/org_id/{org_id}/page/{page}/limit/{limit}")
|
||||
async def api_get_collections_by(
|
||||
request: Request,
|
||||
page: int,
|
||||
limit: int,
|
||||
org_id: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Get collections by page and limit
|
||||
"""
|
||||
return await get_collections(request, org_id, current_user, page, limit)
|
||||
|
||||
|
||||
@router.put("/{collection_id}")
|
||||
async def api_update_collection(
|
||||
request: Request,
|
||||
collection_object: Collection,
|
||||
collection_id: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Update collection by ID
|
||||
"""
|
||||
return await update_collection(
|
||||
request, collection_object, collection_id, current_user
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{collection_id}")
|
||||
async def api_delete_collection(
|
||||
request: Request,
|
||||
collection_id: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Delete collection by ID
|
||||
"""
|
||||
|
||||
return await delete_collection(request, collection_id, current_user)
|
||||
66
apps/api/src/routers/courses/courses.py
Normal file
66
apps/api/src/routers/courses/courses.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
from fastapi import APIRouter, Depends, UploadFile, Form, Request
|
||||
from src.security.auth import get_current_user
|
||||
|
||||
from src.services.courses.courses import Course, create_course, get_course, get_course_meta, get_courses_orgslug, update_course, delete_course, update_course_thumbnail
|
||||
from src.services.users.users import PublicUser
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def api_create_course(request: Request, org_id: str, name: str = Form(), mini_description: str = Form(), description: str = Form(), public: bool = Form(), current_user: PublicUser = Depends(get_current_user), thumbnail: UploadFile | None = None):
|
||||
"""
|
||||
Create new Course
|
||||
"""
|
||||
course = Course(name=name, mini_description=mini_description, description=description,
|
||||
org_id=org_id, public=public, thumbnail="", chapters=[], chapters_content=[], learnings=[])
|
||||
return await create_course(request, course, org_id, current_user, thumbnail)
|
||||
|
||||
|
||||
@router.put("/thumbnail/{course_id}")
|
||||
async def api_create_course_thumbnail(request: Request, course_id: str, thumbnail: UploadFile | None = None, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Update new Course Thumbnail
|
||||
"""
|
||||
return await update_course_thumbnail(request, course_id, current_user, thumbnail)
|
||||
|
||||
|
||||
@router.get("/{course_id}")
|
||||
async def api_get_course(request: Request, course_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get single Course by course_id
|
||||
"""
|
||||
return await get_course(request, course_id, current_user=current_user)
|
||||
|
||||
|
||||
@router.get("/meta/{course_id}")
|
||||
async def api_get_course_meta(request: Request, course_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get single Course Metadata (chapters, activities) by course_id
|
||||
"""
|
||||
return await get_course_meta(request, course_id, current_user=current_user)
|
||||
|
||||
@router.get("/org_slug/{org_slug}/page/{page}/limit/{limit}")
|
||||
async def api_get_course_by_orgslug(request: Request, page: int, limit: int, org_slug: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get houses by page and limit
|
||||
"""
|
||||
return await get_courses_orgslug(request, current_user, page, limit, org_slug)
|
||||
|
||||
|
||||
@router.put("/{course_id}")
|
||||
async def api_update_course(request: Request, course_object: Course, course_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Update Course by course_id
|
||||
"""
|
||||
return await update_course(request, course_object, course_id, current_user)
|
||||
|
||||
|
||||
@router.delete("/{course_id}")
|
||||
async def api_delete_course(request: Request, course_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Delete Course by ID
|
||||
"""
|
||||
|
||||
return await delete_course(request, course_id, current_user)
|
||||
18
apps/api/src/routers/dev.py
Normal file
18
apps/api/src/routers/dev.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
from fastapi import APIRouter, Request
|
||||
from config.config import get_learnhouse_config
|
||||
from src.services.dev.mocks.initial import create_initial_data
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/config")
|
||||
async def config():
|
||||
config = get_learnhouse_config()
|
||||
return config.dict()
|
||||
|
||||
|
||||
@router.get("/mock/initial")
|
||||
async def initial_data(request: Request):
|
||||
await create_initial_data(request)
|
||||
return {"Message": "Initial data created 🤖"}
|
||||
0
apps/api/src/routers/install/__init__.py
Normal file
0
apps/api/src/routers/install/__init__.py
Normal file
70
apps/api/src/routers/install/install.py
Normal file
70
apps/api/src/routers/install/install.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
from fastapi import APIRouter, Request
|
||||
|
||||
from src.services.install.install import (
|
||||
create_install_instance,
|
||||
create_sample_data,
|
||||
get_latest_install_instance,
|
||||
install_create_organization,
|
||||
install_create_organization_user,
|
||||
install_default_elements,
|
||||
update_install_instance,
|
||||
)
|
||||
from src.services.orgs.schemas.orgs import Organization
|
||||
from src.services.users.schemas.users import UserWithPassword
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/start")
|
||||
async def api_create_install_instance(request: Request, data: dict):
|
||||
# create install
|
||||
install = await create_install_instance(request, data)
|
||||
|
||||
return install
|
||||
|
||||
|
||||
@router.get("/latest")
|
||||
async def api_get_latest_install_instance(request: Request):
|
||||
# get latest created install
|
||||
install = await get_latest_install_instance(request)
|
||||
|
||||
return install
|
||||
|
||||
|
||||
@router.post("/default_elements")
|
||||
async def api_install_def_elements(request: Request):
|
||||
elements = await install_default_elements(request, {})
|
||||
|
||||
return elements
|
||||
|
||||
|
||||
@router.post("/org")
|
||||
async def api_install_org(request: Request, org: Organization):
|
||||
organization = await install_create_organization(request, org)
|
||||
|
||||
return organization
|
||||
|
||||
|
||||
@router.post("/user")
|
||||
async def api_install_user(request: Request, data: UserWithPassword, org_slug: str):
|
||||
user = await install_create_organization_user(request, data, org_slug)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
@router.post("/sample")
|
||||
async def api_install_user_sample(request: Request, username: str, org_slug: str):
|
||||
sample = await create_sample_data(org_slug, username, request)
|
||||
|
||||
return sample
|
||||
|
||||
|
||||
@router.post("/update")
|
||||
async def api_update_install_instance(request: Request, data: dict, step: int):
|
||||
request.app.db["installs"]
|
||||
|
||||
# get latest created install
|
||||
install = await update_install_instance(request, data, step)
|
||||
|
||||
return install
|
||||
63
apps/api/src/routers/orgs.py
Normal file
63
apps/api/src/routers/orgs.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
|
||||
from fastapi import APIRouter, Depends, Request, UploadFile
|
||||
from src.security.auth import get_current_user
|
||||
from src.services.orgs.orgs import Organization, create_org, delete_org, get_organization, get_organization_by_slug, get_orgs_by_user, update_org, update_org_logo
|
||||
from src.services.users.users import PublicUser, User
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def api_create_org(request: Request, org_object: Organization, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Create new organization
|
||||
"""
|
||||
return await create_org(request, org_object, current_user)
|
||||
|
||||
|
||||
@router.get("/{org_id}")
|
||||
async def api_get_org(request: Request, org_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get single Org by ID
|
||||
"""
|
||||
return await get_organization(request, org_id)
|
||||
|
||||
|
||||
@router.get("/slug/{org_slug}")
|
||||
async def api_get_org_by_slug(request: Request, org_slug: str, current_user: User = Depends(get_current_user)):
|
||||
"""
|
||||
Get single Org by Slug
|
||||
"""
|
||||
return await get_organization_by_slug(request, org_slug)
|
||||
|
||||
@router.put("/{org_id}/logo")
|
||||
async def api_update_org_logo(request: Request, org_id: str, logo_file:UploadFile, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get single Org by Slug
|
||||
"""
|
||||
return await update_org_logo(request=request,logo_file=logo_file, org_id=org_id, current_user=current_user)
|
||||
|
||||
@router.get("/user/page/{page}/limit/{limit}")
|
||||
async def api_user_orgs(request: Request, page: int, limit: int, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get orgs by page and limit by user
|
||||
"""
|
||||
return await get_orgs_by_user(request, current_user.user_id, page, limit)
|
||||
|
||||
|
||||
@router.put("/{org_id}")
|
||||
async def api_update_org(request: Request, org_object: Organization, org_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Update Org by ID
|
||||
"""
|
||||
return await update_org(request, org_object, org_id, current_user)
|
||||
|
||||
|
||||
@router.delete("/{org_id}")
|
||||
async def api_delete_org(request: Request, org_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Delete Org by ID
|
||||
"""
|
||||
|
||||
return await delete_org(request, org_id, current_user)
|
||||
41
apps/api/src/routers/roles.py
Normal file
41
apps/api/src/routers/roles.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
from fastapi import APIRouter, Depends, Request
|
||||
from src.security.auth import get_current_user
|
||||
from src.services.roles.schemas.roles import Role
|
||||
from src.services.roles.roles import create_role, delete_role, read_role, update_role
|
||||
from src.services.users.schemas.users import PublicUser
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def api_create_role(request: Request, role_object: Role, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Create new role
|
||||
"""
|
||||
return await create_role(request, role_object, current_user)
|
||||
|
||||
|
||||
@router.get("/{role_id}")
|
||||
async def api_get_role(request: Request, role_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Get single role by role_id
|
||||
"""
|
||||
return await read_role(request, role_id, current_user)
|
||||
|
||||
|
||||
@router.put("/{role_id}")
|
||||
async def api_update_role(request: Request, role_object: Role, role_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Update role by role_id
|
||||
"""
|
||||
return await update_role(request, role_id, role_object, current_user)
|
||||
|
||||
|
||||
@router.delete("/{role_id}")
|
||||
async def api_delete_role(request: Request, role_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Delete role by ID
|
||||
"""
|
||||
|
||||
return await delete_role(request, role_id, current_user)
|
||||
56
apps/api/src/routers/trail.py
Normal file
56
apps/api/src/routers/trail.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
from fastapi import APIRouter, Depends, Request
|
||||
from src.security.auth import get_current_user
|
||||
from src.services.trail.trail import Trail, add_activity_to_trail, add_course_to_trail, create_trail, get_user_trail_with_orgslug, get_user_trail, remove_course_from_trail
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/start")
|
||||
async def api_start_trail(request: Request, trail_object: Trail, org_id: str, user=Depends(get_current_user)) -> Trail:
|
||||
"""
|
||||
Start trail
|
||||
"""
|
||||
return await create_trail(request, user, org_id, trail_object)
|
||||
|
||||
|
||||
@router.get("/org_id/{org_id}/trail")
|
||||
async def api_get_trail_by_orgid(request: Request, org_slug: str, user=Depends(get_current_user)):
|
||||
"""
|
||||
Get a user trails
|
||||
"""
|
||||
return await get_user_trail(request, user=user, org_slug=org_slug)
|
||||
|
||||
|
||||
@router.get("/org_slug/{org_slug}/trail")
|
||||
async def api_get_trail_by_orgslug(request: Request, org_slug: str, user=Depends(get_current_user)):
|
||||
"""
|
||||
Get a user trails using org slug
|
||||
"""
|
||||
return await get_user_trail_with_orgslug(request, user, org_slug=org_slug)
|
||||
|
||||
# Courses in trail
|
||||
|
||||
|
||||
@router.post("/org_slug/{org_slug}/add_course/{course_id}")
|
||||
async def api_add_course_to_trail(request: Request, course_id: str, org_slug: str, user=Depends(get_current_user)):
|
||||
"""
|
||||
Add Course to trail
|
||||
"""
|
||||
return await add_course_to_trail(request, user, org_slug, course_id)
|
||||
|
||||
|
||||
@router.post("/org_slug/{org_slug}/remove_course/{course_id}")
|
||||
async def api_remove_course_to_trail(request: Request, course_id: str, org_slug: str, user=Depends(get_current_user)):
|
||||
"""
|
||||
Remove Course from trail
|
||||
"""
|
||||
return await remove_course_from_trail(request, user, org_slug, course_id)
|
||||
|
||||
|
||||
@router.post("/org_slug/{org_slug}/add_activity/course_id/{course_id}/activity_id/{activity_id}")
|
||||
async def api_add_activity_to_trail(request: Request, activity_id: str, course_id: str, org_slug: str, user=Depends(get_current_user)):
|
||||
"""
|
||||
Add Course to trail
|
||||
"""
|
||||
return await add_activity_to_trail(request, user, course_id, org_slug, activity_id)
|
||||
66
apps/api/src/routers/users.py
Normal file
66
apps/api/src/routers/users.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
from fastapi import Depends, APIRouter, Request
|
||||
from src.security.auth import get_current_user
|
||||
from src.services.users.schemas.users import PasswordChangeForm, PublicUser, User, UserWithPassword
|
||||
from src.services.users.users import create_user, delete_user, get_profile_metadata, get_user_by_userid, update_user, update_user_password
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/profile")
|
||||
async def api_get_current_user(current_user: User = Depends(get_current_user)):
|
||||
"""
|
||||
Get current user
|
||||
"""
|
||||
return current_user.dict()
|
||||
|
||||
@router.get("/profile_metadata")
|
||||
async def api_get_current_user_metadata(request: Request,current_user: User = Depends(get_current_user)):
|
||||
"""
|
||||
Get current user
|
||||
"""
|
||||
return await get_profile_metadata(request , current_user.dict())
|
||||
|
||||
|
||||
|
||||
@router.get("/user_id/{user_id}")
|
||||
async def api_get_user_by_userid(request: Request,user_id: str):
|
||||
"""
|
||||
Get single user by user_id
|
||||
"""
|
||||
return await get_user_by_userid(request, user_id)
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def api_create_user(request: Request,user_object: UserWithPassword, org_slug: str ):
|
||||
"""
|
||||
Create new user
|
||||
"""
|
||||
return await create_user(request, None, user_object, org_slug)
|
||||
|
||||
|
||||
@router.delete("/user_id/{user_id}")
|
||||
async def api_delete_user(request: Request, user_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Delete user by ID
|
||||
"""
|
||||
|
||||
return await delete_user(request, current_user, user_id)
|
||||
|
||||
|
||||
@router.put("/user_id/{user_id}")
|
||||
async def api_update_user(request: Request, user_object: User, user_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Update user by ID
|
||||
"""
|
||||
return await update_user(request, user_id, user_object, current_user)
|
||||
|
||||
@router.put("/password/user_id/{user_id}")
|
||||
async def api_update_user_password(request: Request, user_id: str , passwordChangeForm : PasswordChangeForm, current_user: PublicUser = Depends(get_current_user)):
|
||||
"""
|
||||
Update user password by ID
|
||||
"""
|
||||
return await update_user_password(request,current_user, user_id, passwordChangeForm)
|
||||
0
apps/api/src/security/__init__.py
Normal file
0
apps/api/src/security/__init__.py
Normal file
94
apps/api/src/security/auth.py
Normal file
94
apps/api/src/security/auth.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
from config.config import get_learnhouse_config
|
||||
from pydantic import BaseModel
|
||||
from fastapi import Depends, HTTPException, Request, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jose import JWTError, jwt
|
||||
from datetime import datetime, timedelta
|
||||
from src.services.dev.dev import isDevModeEnabled
|
||||
from src.services.users.schemas.users import AnonymousUser, PublicUser
|
||||
from src.services.users.users import security_get_user, security_verify_password
|
||||
from src.security.security import ALGORITHM, SECRET_KEY
|
||||
from fastapi_jwt_auth import AuthJWT
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
|
||||
|
||||
|
||||
#### JWT Auth ####################################################
|
||||
class Settings(BaseModel):
|
||||
authjwt_secret_key: str = "secret" if isDevModeEnabled() else SECRET_KEY
|
||||
authjwt_token_location = {"cookies", "headers"}
|
||||
authjwt_cookie_csrf_protect = False
|
||||
authjwt_access_token_expires = False if isDevModeEnabled() else 28800
|
||||
authjwt_cookie_samesite = "lax"
|
||||
authjwt_cookie_secure = True
|
||||
authjwt_cookie_domain = get_learnhouse_config().hosting_config.cookie_config.domain
|
||||
|
||||
|
||||
@AuthJWT.load_config # type: ignore
|
||||
def get_config():
|
||||
return Settings()
|
||||
|
||||
|
||||
#### JWT Auth ####################################################
|
||||
|
||||
|
||||
#### Classes ####################################################
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
|
||||
class TokenData(BaseModel):
|
||||
username: str | None = None
|
||||
|
||||
|
||||
#### Classes ####################################################
|
||||
|
||||
|
||||
async def authenticate_user(request: Request, email: str, password: str):
|
||||
user = await security_get_user(request, email)
|
||||
if not user:
|
||||
return False
|
||||
if not await security_verify_password(password, user.password):
|
||||
return False
|
||||
return user
|
||||
|
||||
|
||||
def create_access_token(data: dict, expires_delta: timedelta | None = None):
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=15)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
async def get_current_user(request: Request, Authorize: AuthJWT = Depends()):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
try:
|
||||
Authorize.jwt_optional()
|
||||
username = Authorize.get_jwt_subject() or None
|
||||
token_data = TokenData(username=username) # type: ignore
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
if username:
|
||||
user = await security_get_user(request, email=token_data.username) # type: ignore # treated as an email
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return PublicUser(**user.dict())
|
||||
else:
|
||||
return AnonymousUser()
|
||||
|
||||
|
||||
async def non_public_endpoint(current_user: PublicUser):
|
||||
if isinstance(current_user, AnonymousUser):
|
||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||
149
apps/api/src/security/rbac/rbac.py
Normal file
149
apps/api/src/security/rbac/rbac.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
from typing import Literal
|
||||
from fastapi import HTTPException, status, Request
|
||||
from src.security.rbac.utils import check_element_type, get_id_identifier_of_element
|
||||
from src.services.roles.schemas.roles import RoleInDB
|
||||
from src.services.users.schemas.users import UserRolesInOrganization
|
||||
|
||||
|
||||
async def authorization_verify_if_element_is_public(
|
||||
request,
|
||||
element_id: str,
|
||||
user_id: str,
|
||||
action: Literal["read"],
|
||||
):
|
||||
element_nature = await check_element_type(element_id)
|
||||
|
||||
# Verifies if the element is public
|
||||
if (
|
||||
element_nature == ("courses" or "collections")
|
||||
and action == "read"
|
||||
and user_id == "anonymous"
|
||||
):
|
||||
if element_nature == "courses":
|
||||
courses = request.app.db["courses"]
|
||||
course = await courses.find_one({"course_id": element_id})
|
||||
|
||||
if course["public"]:
|
||||
return True
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User rights (public content) : You don't have the right to perform this action",
|
||||
)
|
||||
|
||||
if element_nature == "collections":
|
||||
collections = request.app.db["collections"]
|
||||
collection = await collections.find_one({"collection_id": element_id})
|
||||
|
||||
if collection["public"]:
|
||||
return True
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User rights (public content) : You don't have the right to perform this action",
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User rights (public content) : You don't have the right to perform this action",
|
||||
)
|
||||
|
||||
|
||||
async def authorization_verify_if_user_is_author(
|
||||
request,
|
||||
user_id: str,
|
||||
action: Literal["read", "update", "delete", "create"],
|
||||
element_id: str,
|
||||
):
|
||||
if action == "update" or "delete" or "read":
|
||||
element_nature = await check_element_type(element_id)
|
||||
elements = request.app.db[element_nature]
|
||||
element_identifier = await get_id_identifier_of_element(element_id)
|
||||
element = await elements.find_one({element_identifier: element_id})
|
||||
if user_id in element["authors"]:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
async def authorization_verify_based_on_roles(
|
||||
request: Request,
|
||||
user_id: str,
|
||||
action: Literal["read", "update", "delete", "create"],
|
||||
roles_list: list[UserRolesInOrganization],
|
||||
element_id: str,
|
||||
):
|
||||
element_type = await check_element_type(element_id)
|
||||
element = request.app.db[element_type]
|
||||
roles = request.app.db["roles"]
|
||||
|
||||
# Get the element
|
||||
element_identifier = await get_id_identifier_of_element(element_id)
|
||||
element = await element.find_one({element_identifier: element_id})
|
||||
|
||||
# Get the roles of the user
|
||||
roles_id_list = [role["role_id"] for role in roles_list]
|
||||
roles = await roles.find({"role_id": {"$in": roles_id_list}}).to_list(length=100)
|
||||
|
||||
async def checkRoles():
|
||||
# Check Roles
|
||||
for role in roles:
|
||||
role = RoleInDB(**role)
|
||||
if role.elements[element_type][f"action_{action}"] is True:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
async def checkOrgRoles():
|
||||
# Check Org Roles
|
||||
users = request.app.db["users"]
|
||||
user = await users.find_one({"user_id": user_id})
|
||||
if element is not None:
|
||||
for org in user["orgs"]:
|
||||
if org["org_id"] == element["org_id"]:
|
||||
if org["org_role"] == "owner" or org["org_role"] == "editor":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
if await checkRoles() or await checkOrgRoles():
|
||||
return True
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User rights (roless) : You don't have the right to perform this action",
|
||||
)
|
||||
|
||||
|
||||
async def authorization_verify_based_on_roles_and_authorship(
|
||||
request: Request,
|
||||
user_id: str,
|
||||
action: Literal["read", "update", "delete", "create"],
|
||||
roles_list: list[UserRolesInOrganization],
|
||||
element_id: str,
|
||||
):
|
||||
isAuthor = await authorization_verify_if_user_is_author(
|
||||
request, user_id, action, element_id
|
||||
)
|
||||
|
||||
isRole = await authorization_verify_based_on_roles(
|
||||
request, user_id, action, roles_list, element_id
|
||||
)
|
||||
|
||||
if isAuthor or isRole:
|
||||
return True
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User rights (roles & authorship) : You don't have the right to perform this action",
|
||||
)
|
||||
|
||||
|
||||
async def authorization_verify_if_user_is_anon(user_id: str):
|
||||
if user_id == "anonymous":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="You should be logged in to perform this action",
|
||||
)
|
||||
45
apps/api/src/security/rbac/utils.py
Normal file
45
apps/api/src/security/rbac/utils.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
from fastapi import HTTPException, status
|
||||
|
||||
|
||||
async def check_element_type(element_id):
|
||||
"""
|
||||
Check if the element is a course, a user, a house or a collection, by checking its prefix
|
||||
"""
|
||||
if element_id.startswith("course_"):
|
||||
return "courses"
|
||||
elif element_id.startswith("user_"):
|
||||
return "users"
|
||||
elif element_id.startswith("house_"):
|
||||
return "houses"
|
||||
elif element_id.startswith("org_"):
|
||||
return "organizations"
|
||||
elif element_id.startswith("coursechapter_"):
|
||||
return "coursechapters"
|
||||
elif element_id.startswith("collection_"):
|
||||
return "collections"
|
||||
elif element_id.startswith("activity_"):
|
||||
return "activities"
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="User rights : Issue verifying element nature",
|
||||
)
|
||||
|
||||
|
||||
async def get_singular_form_of_element(element_id):
|
||||
element_type = await check_element_type(element_id)
|
||||
|
||||
if element_type == "activities":
|
||||
return "activity"
|
||||
else:
|
||||
singular_form_element = element_type[:-1]
|
||||
return singular_form_element
|
||||
|
||||
|
||||
async def get_id_identifier_of_element(element_id):
|
||||
singular_form_element = await get_singular_form_of_element(element_id)
|
||||
|
||||
if singular_form_element == "ogranizations":
|
||||
return "org_id"
|
||||
else:
|
||||
return str(singular_form_element) + "_id"
|
||||
30
apps/api/src/security/security.py
Normal file
30
apps/api/src/security/security.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
from passlib.context import CryptContext
|
||||
from passlib.hash import pbkdf2_sha256
|
||||
from config.config import get_learnhouse_config
|
||||
|
||||
|
||||
### 🔒 JWT ##############################################################
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
||||
SECRET_KEY = get_learnhouse_config().security_config.auth_jwt_secret_key
|
||||
ALGORITHM = "HS256"
|
||||
|
||||
### 🔒 JWT ##############################################################
|
||||
|
||||
|
||||
### 🔒 Passwords Hashing ##############################################################
|
||||
|
||||
|
||||
async def security_hash_password(password: str):
|
||||
return pbkdf2_sha256.hash(password)
|
||||
|
||||
|
||||
async def security_verify_password(plain_password: str, hashed_password: str):
|
||||
return pbkdf2_sha256.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
### 🔒 Passwords Hashing ##############################################################
|
||||
|
||||
|
||||
0
apps/api/src/services/__init__.py
Normal file
0
apps/api/src/services/__init__.py
Normal file
0
apps/api/src/services/blocks/__init__.py
Normal file
0
apps/api/src/services/blocks/__init__.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
from uuid import uuid4
|
||||
from fastapi import HTTPException, status, UploadFile, Request
|
||||
from src.services.blocks.schemas.blocks import Block
|
||||
from src.services.blocks.utils.upload_files import upload_file_and_return_file_object
|
||||
|
||||
from src.services.users.users import PublicUser
|
||||
|
||||
|
||||
async def create_image_block(
|
||||
request: Request, image_file: UploadFile, activity_id: str
|
||||
):
|
||||
blocks = request.app.db["blocks"]
|
||||
activity = request.app.db["activities"]
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
block_type = "imageBlock"
|
||||
|
||||
# get org_id from activity
|
||||
activity = await activity.find_one({"activity_id": activity_id}, {"_id": 0})
|
||||
org_id = activity["org_id"]
|
||||
|
||||
coursechapter_id = activity["coursechapter_id"]
|
||||
|
||||
# get course_id from coursechapter
|
||||
course = await courses.find_one(
|
||||
{"chapters": coursechapter_id},
|
||||
{"_id": 0},
|
||||
)
|
||||
|
||||
|
||||
# get block id
|
||||
block_id = str(f"block_{uuid4()}")
|
||||
|
||||
block_data = await upload_file_and_return_file_object(
|
||||
request,
|
||||
image_file,
|
||||
activity_id,
|
||||
block_id,
|
||||
["jpg", "jpeg", "png", "gif"],
|
||||
block_type,
|
||||
org_id,
|
||||
course["course_id"],
|
||||
)
|
||||
|
||||
# create block
|
||||
block = Block(
|
||||
block_id=block_id,
|
||||
activity_id=activity_id,
|
||||
block_type=block_type,
|
||||
block_data=block_data,
|
||||
org_id=org_id,
|
||||
course_id=course["course_id"],
|
||||
)
|
||||
|
||||
# insert block
|
||||
await blocks.insert_one(block.dict())
|
||||
|
||||
return block
|
||||
|
||||
|
||||
async def get_image_block(request: Request, file_id: str, current_user: PublicUser):
|
||||
blocks = request.app.db["blocks"]
|
||||
|
||||
video_block = await blocks.find_one({"block_id": file_id})
|
||||
|
||||
if video_block:
|
||||
return Block(**video_block)
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Image block does not exist"
|
||||
)
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
from uuid import uuid4
|
||||
from fastapi import HTTPException, status, UploadFile, Request
|
||||
from src.services.blocks.schemas.blocks import Block
|
||||
from src.services.blocks.utils.upload_files import upload_file_and_return_file_object
|
||||
|
||||
from src.services.users.users import PublicUser
|
||||
|
||||
|
||||
async def create_pdf_block(request: Request, pdf_file: UploadFile, activity_id: str):
|
||||
blocks = request.app.db["blocks"]
|
||||
activity = request.app.db["activities"]
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
block_type = "pdfBlock"
|
||||
|
||||
# get org_id from activity
|
||||
activity = await activity.find_one({"activity_id": activity_id}, {"_id": 0})
|
||||
org_id = activity["org_id"]
|
||||
|
||||
# get block id
|
||||
block_id = str(f"block_{uuid4()}")
|
||||
|
||||
coursechapter_id = activity["coursechapter_id"]
|
||||
|
||||
# get course_id from coursechapter
|
||||
course = await courses.find_one(
|
||||
{"chapters": coursechapter_id},
|
||||
{"_id": 0},
|
||||
)
|
||||
|
||||
block_data = await upload_file_and_return_file_object(
|
||||
request,
|
||||
pdf_file,
|
||||
activity_id,
|
||||
block_id,
|
||||
["pdf"],
|
||||
block_type,
|
||||
org_id,
|
||||
course["course_id"],
|
||||
)
|
||||
|
||||
# create block
|
||||
block = Block(
|
||||
block_id=block_id,
|
||||
activity_id=activity_id,
|
||||
block_type=block_type,
|
||||
block_data=block_data,
|
||||
org_id=org_id,
|
||||
course_id=course["course_id"],
|
||||
)
|
||||
|
||||
# insert block
|
||||
await blocks.insert_one(block.dict())
|
||||
|
||||
return block
|
||||
|
||||
|
||||
async def get_pdf_block(request: Request, file_id: str, current_user: PublicUser):
|
||||
blocks = request.app.db["blocks"]
|
||||
|
||||
pdf_block = await blocks.find_one({"block_id": file_id})
|
||||
|
||||
if pdf_block:
|
||||
return Block(**pdf_block)
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Video file does not exist"
|
||||
)
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
from typing import List, Literal
|
||||
from uuid import uuid4
|
||||
from fastapi import Request
|
||||
from pydantic import BaseModel
|
||||
from src.services.blocks.schemas.blocks import Block
|
||||
from src.services.users.users import PublicUser
|
||||
|
||||
|
||||
class option(BaseModel):
|
||||
option_id: str
|
||||
option_type: Literal["text", "image"]
|
||||
option_data: str
|
||||
|
||||
|
||||
class answer(BaseModel):
|
||||
question_id: str
|
||||
option_id: str
|
||||
|
||||
|
||||
class question(BaseModel):
|
||||
question_id: str
|
||||
question_value:str
|
||||
options: List[option]
|
||||
|
||||
|
||||
class quizBlock(BaseModel):
|
||||
questions: List[question]
|
||||
answers: List[answer]
|
||||
|
||||
|
||||
async def create_quiz_block(request: Request, quizBlock: quizBlock, activity_id: str, user: PublicUser):
|
||||
blocks = request.app.db["blocks"]
|
||||
activities = request.app.db["activities"]
|
||||
request.app.db["courses"]
|
||||
|
||||
# Get org_id from activity
|
||||
activity = await activities.find_one({"activity_id": activity_id}, {"_id": 0, "org_id": 1})
|
||||
org_id = activity["org_id"]
|
||||
|
||||
# Get course_id from activity
|
||||
course = await activities.find_one({"activity_id": activity_id}, {"_id": 0, "course_id": 1})
|
||||
|
||||
block_id = str(f"block_{uuid4()}")
|
||||
|
||||
# create block
|
||||
block = Block(block_id=block_id, activity_id=activity_id,
|
||||
block_type="quizBlock", block_data=quizBlock, org_id=org_id, course_id=course["course_id"])
|
||||
|
||||
# insert block
|
||||
await blocks.insert_one(block.dict())
|
||||
|
||||
return block
|
||||
|
||||
|
||||
async def get_quiz_block_options(request: Request, block_id: str, user: PublicUser):
|
||||
blocks = request.app.db["blocks"]
|
||||
# find block but get only the options
|
||||
block = await blocks.find_one({"block_id": block_id, }, {
|
||||
"_id": 0, "block_data.answers": 0})
|
||||
|
||||
return block
|
||||
|
||||
async def get_quiz_block_answers(request: Request, block_id: str, user: PublicUser):
|
||||
blocks = request.app.db["blocks"]
|
||||
|
||||
# find block but get only the answers
|
||||
block = await blocks.find_one({"block_id": block_id, }, {
|
||||
"_id": 0, "block_data.questions": 0})
|
||||
|
||||
return block
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
from uuid import uuid4
|
||||
from fastapi import HTTPException, status, UploadFile, Request
|
||||
from src.services.blocks.schemas.blocks import Block
|
||||
from src.services.blocks.utils.upload_files import upload_file_and_return_file_object
|
||||
|
||||
from src.services.users.users import PublicUser
|
||||
|
||||
|
||||
async def create_video_block(
|
||||
request: Request, video_file: UploadFile, activity_id: str
|
||||
):
|
||||
blocks = request.app.db["blocks"]
|
||||
activity = request.app.db["activities"]
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
block_type = "videoBlock"
|
||||
|
||||
# get org_id from activity
|
||||
activity = await activity.find_one(
|
||||
{"activity_id": activity_id}, {"_id": 0}
|
||||
)
|
||||
org_id = activity["org_id"]
|
||||
|
||||
# get block id
|
||||
block_id = str(f"block_{uuid4()}")
|
||||
|
||||
coursechapter_id = activity["coursechapter_id"]
|
||||
|
||||
# get course_id from coursechapter
|
||||
course = await courses.find_one(
|
||||
{"chapters": coursechapter_id},
|
||||
{"_id": 0},
|
||||
)
|
||||
|
||||
block_data = await upload_file_and_return_file_object(
|
||||
request,
|
||||
video_file,
|
||||
activity_id,
|
||||
block_id,
|
||||
["mp4", "webm", "ogg"],
|
||||
block_type,
|
||||
org_id,
|
||||
course["course_id"],
|
||||
)
|
||||
|
||||
# create block
|
||||
block = Block(
|
||||
block_id=block_id,
|
||||
activity_id=activity_id,
|
||||
block_type=block_type,
|
||||
block_data=block_data,
|
||||
org_id=org_id,
|
||||
course_id=course["course_id"],
|
||||
)
|
||||
|
||||
# insert block
|
||||
await blocks.insert_one(block.dict())
|
||||
|
||||
return block
|
||||
|
||||
|
||||
async def get_video_block(request: Request, file_id: str, current_user: PublicUser):
|
||||
blocks = request.app.db["blocks"]
|
||||
|
||||
video_block = await blocks.find_one({"block_id": file_id})
|
||||
|
||||
if video_block:
|
||||
return Block(**video_block)
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Video file does not exist"
|
||||
)
|
||||
12
apps/api/src/services/blocks/schemas/blocks.py
Normal file
12
apps/api/src/services/blocks/schemas/blocks.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
|
||||
from typing import Any, Literal
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Block(BaseModel):
|
||||
block_id: str
|
||||
activity_id: str
|
||||
course_id: str
|
||||
org_id: str
|
||||
block_type: Literal["quizBlock", "videoBlock", "pdfBlock", "imageBlock"]
|
||||
block_data: Any
|
||||
10
apps/api/src/services/blocks/schemas/files.py
Normal file
10
apps/api/src/services/blocks/schemas/files.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class BlockFile(BaseModel):
|
||||
file_id: str
|
||||
file_format: str
|
||||
file_name: str
|
||||
file_size: int
|
||||
file_type: str
|
||||
activity_id: str
|
||||
58
apps/api/src/services/blocks/utils/upload_files.py
Normal file
58
apps/api/src/services/blocks/utils/upload_files.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import uuid
|
||||
from fastapi import HTTPException, Request, UploadFile, status
|
||||
from src.services.blocks.schemas.files import BlockFile
|
||||
from src.services.utils.upload_content import upload_content
|
||||
|
||||
|
||||
async def upload_file_and_return_file_object(
|
||||
request: Request,
|
||||
file: UploadFile,
|
||||
activity_id: str,
|
||||
block_id: str,
|
||||
list_of_allowed_file_formats: list,
|
||||
type_of_block: str,
|
||||
org_id: str,
|
||||
course_id: str,
|
||||
):
|
||||
# get file id
|
||||
file_id = str(uuid.uuid4())
|
||||
|
||||
# get file format
|
||||
file_format = file.filename.split(".")[-1]
|
||||
|
||||
# validate file format
|
||||
if file_format not in list_of_allowed_file_formats:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="File format not supported"
|
||||
)
|
||||
|
||||
# create file
|
||||
file_binary = await file.read()
|
||||
|
||||
# get file size
|
||||
file_size = len(await file.read())
|
||||
|
||||
# get file type
|
||||
file_type = file.content_type
|
||||
|
||||
# get file name
|
||||
file_name = file.filename
|
||||
|
||||
# create file object
|
||||
uploadable_file = BlockFile(
|
||||
file_id=file_id,
|
||||
file_format=file_format,
|
||||
file_name=file_name,
|
||||
file_size=file_size,
|
||||
file_type=file_type,
|
||||
activity_id=activity_id,
|
||||
)
|
||||
|
||||
await upload_content(
|
||||
f"courses/{course_id}/activities/{activity_id}/dynamic/blocks/{type_of_block}/{block_id}",
|
||||
org_id=org_id,
|
||||
file_binary=file_binary,
|
||||
file_and_format=f"{file_id}.{file_format}",
|
||||
)
|
||||
|
||||
return uploadable_file
|
||||
250
apps/api/src/services/courses/activities/activities.py
Normal file
250
apps/api/src/services/courses/activities/activities.py
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
from typing import Literal
|
||||
from pydantic import BaseModel
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles,
|
||||
authorization_verify_if_element_is_public,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
from src.services.users.schemas.users import AnonymousUser, PublicUser
|
||||
from fastapi import HTTPException, status, Request
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
|
||||
#### Classes ####################################################
|
||||
|
||||
|
||||
class Activity(BaseModel):
|
||||
name: str
|
||||
type: str
|
||||
content: object
|
||||
|
||||
|
||||
class ActivityInDB(Activity):
|
||||
activity_id: str
|
||||
course_id: str
|
||||
coursechapter_id: str
|
||||
org_id: str
|
||||
creationDate: str
|
||||
updateDate: str
|
||||
|
||||
|
||||
#### Classes ####################################################
|
||||
|
||||
|
||||
####################################################
|
||||
# CRUD
|
||||
####################################################
|
||||
|
||||
|
||||
async def create_activity(
|
||||
request: Request,
|
||||
activity_object: Activity,
|
||||
org_id: str,
|
||||
coursechapter_id: str,
|
||||
current_user: PublicUser,
|
||||
):
|
||||
activities = request.app.db["activities"]
|
||||
courses = request.app.db["courses"]
|
||||
users = request.app.db["users"]
|
||||
|
||||
# get user
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
# generate activity_id
|
||||
activity_id = str(f"activity_{uuid4()}")
|
||||
|
||||
# verify activity rights
|
||||
await authorization_verify_based_on_roles(
|
||||
request,
|
||||
current_user.user_id,
|
||||
"create",
|
||||
user["roles"],
|
||||
activity_id,
|
||||
)
|
||||
|
||||
# get course_id from activity
|
||||
course = await courses.find_one({"chapters": coursechapter_id})
|
||||
|
||||
# create activity
|
||||
activity = ActivityInDB(
|
||||
**activity_object.dict(),
|
||||
creationDate=str(datetime.now()),
|
||||
coursechapter_id=coursechapter_id,
|
||||
updateDate=str(datetime.now()),
|
||||
activity_id=activity_id,
|
||||
org_id=org_id,
|
||||
course_id=course["course_id"],
|
||||
)
|
||||
await activities.insert_one(activity.dict())
|
||||
|
||||
# update chapter
|
||||
await courses.update_one(
|
||||
{"chapters_content.coursechapter_id": coursechapter_id},
|
||||
{"$addToSet": {"chapters_content.$.activities": activity_id}},
|
||||
)
|
||||
|
||||
return activity
|
||||
|
||||
|
||||
async def get_activity(request: Request, activity_id: str, current_user: PublicUser):
|
||||
activities = request.app.db["activities"]
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
activity = await activities.find_one({"activity_id": activity_id})
|
||||
|
||||
# get course_id from activity
|
||||
coursechapter_id = activity["coursechapter_id"]
|
||||
await courses.find_one({"chapters": coursechapter_id})
|
||||
|
||||
# verify course rights
|
||||
await verify_rights(request, activity["course_id"], current_user, "read")
|
||||
|
||||
if not activity:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
|
||||
)
|
||||
|
||||
activity = ActivityInDB(**activity)
|
||||
return activity
|
||||
|
||||
|
||||
async def update_activity(
|
||||
request: Request,
|
||||
activity_object: Activity,
|
||||
activity_id: str,
|
||||
current_user: PublicUser,
|
||||
):
|
||||
activities = request.app.db["activities"]
|
||||
|
||||
activity = await activities.find_one({"activity_id": activity_id})
|
||||
|
||||
# verify course rights
|
||||
await verify_rights(request, activity_id, current_user, "update")
|
||||
|
||||
if activity:
|
||||
creationDate = activity["creationDate"]
|
||||
|
||||
# get today's date
|
||||
datetime_object = datetime.now()
|
||||
|
||||
updated_course = ActivityInDB(
|
||||
activity_id=activity_id,
|
||||
coursechapter_id=activity["coursechapter_id"],
|
||||
creationDate=creationDate,
|
||||
updateDate=str(datetime_object),
|
||||
course_id=activity["course_id"],
|
||||
org_id=activity["org_id"],
|
||||
**activity_object.dict(),
|
||||
)
|
||||
|
||||
await activities.update_one(
|
||||
{"activity_id": activity_id}, {"$set": updated_course.dict()}
|
||||
)
|
||||
|
||||
return ActivityInDB(**updated_course.dict())
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="activity does not exist"
|
||||
)
|
||||
|
||||
|
||||
async def delete_activity(request: Request, activity_id: str, current_user: PublicUser):
|
||||
activities = request.app.db["activities"]
|
||||
|
||||
activity = await activities.find_one({"activity_id": activity_id})
|
||||
|
||||
# verify course rights
|
||||
await verify_rights(request, activity_id, current_user, "delete")
|
||||
|
||||
if not activity:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="activity does not exist"
|
||||
)
|
||||
|
||||
# Remove Activity
|
||||
isDeleted = await activities.delete_one({"activity_id": activity_id})
|
||||
|
||||
# Remove Activity from chapter
|
||||
courses = request.app.db["courses"]
|
||||
isDeletedFromChapter = await courses.update_one(
|
||||
{"chapters_content.activities": activity_id},
|
||||
{"$pull": {"chapters_content.$.activities": activity_id}},
|
||||
)
|
||||
|
||||
if isDeleted and isDeletedFromChapter:
|
||||
return {"detail": "Activity deleted"}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Unavailable database",
|
||||
)
|
||||
|
||||
|
||||
####################################################
|
||||
# Misc
|
||||
####################################################
|
||||
|
||||
|
||||
async def get_activities(
|
||||
request: Request, coursechapter_id: str, current_user: PublicUser
|
||||
):
|
||||
activities = request.app.db["activities"]
|
||||
|
||||
activities = activities.find({"coursechapter_id": coursechapter_id})
|
||||
|
||||
if not activities:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
|
||||
)
|
||||
|
||||
activities = [
|
||||
ActivityInDB(**activity) for activity in await activities.to_list(length=100)
|
||||
]
|
||||
|
||||
return activities
|
||||
|
||||
|
||||
#### Security ####################################################
|
||||
|
||||
|
||||
async def verify_rights(
|
||||
request: Request,
|
||||
activity_id: str, # course_id in case of read
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
):
|
||||
if action == "read":
|
||||
if current_user.user_id == "anonymous":
|
||||
await authorization_verify_if_element_is_public(
|
||||
request, activity_id, current_user.user_id, action
|
||||
)
|
||||
else:
|
||||
users = request.app.db["users"]
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
await authorization_verify_if_user_is_anon(current_user.user_id)
|
||||
|
||||
await authorization_verify_based_on_roles(
|
||||
request,
|
||||
current_user.user_id,
|
||||
action,
|
||||
user["roles"],
|
||||
activity_id,
|
||||
)
|
||||
else:
|
||||
users = request.app.db["users"]
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
await authorization_verify_if_user_is_anon(current_user.user_id)
|
||||
|
||||
await authorization_verify_based_on_roles(
|
||||
request,
|
||||
current_user.user_id,
|
||||
action,
|
||||
user["roles"],
|
||||
activity_id,
|
||||
)
|
||||
|
||||
|
||||
#### Security ####################################################
|
||||
95
apps/api/src/services/courses/activities/pdf.py
Normal file
95
apps/api/src/services/courses/activities/pdf.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
from src.security.rbac.rbac import authorization_verify_based_on_roles
|
||||
from src.services.courses.activities.uploads.pdfs import upload_pdf
|
||||
from src.services.users.users import PublicUser
|
||||
from src.services.courses.activities.activities import ActivityInDB
|
||||
from fastapi import HTTPException, status, UploadFile, Request
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
async def create_documentpdf_activity(
|
||||
request: Request,
|
||||
name: str,
|
||||
coursechapter_id: str,
|
||||
current_user: PublicUser,
|
||||
pdf_file: UploadFile | None = None,
|
||||
):
|
||||
activities = request.app.db["activities"]
|
||||
courses = request.app.db["courses"]
|
||||
users = request.app.db["users"]
|
||||
|
||||
# get user
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
# generate activity_id
|
||||
activity_id = str(f"activity_{uuid4()}")
|
||||
|
||||
# get org_id from course
|
||||
coursechapter = await courses.find_one(
|
||||
{"chapters_content.coursechapter_id": coursechapter_id}
|
||||
)
|
||||
|
||||
org_id = coursechapter["org_id"]
|
||||
|
||||
# check if pdf_file is not None
|
||||
if not pdf_file:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Pdf : No pdf file provided"
|
||||
)
|
||||
|
||||
if pdf_file.content_type not in ["application/pdf"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Pdf : Wrong pdf format"
|
||||
)
|
||||
|
||||
# get pdf format
|
||||
if pdf_file.filename:
|
||||
pdf_format = pdf_file.filename.split(".")[-1]
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Pdf : No pdf file provided"
|
||||
)
|
||||
|
||||
activity_object = ActivityInDB(
|
||||
org_id=org_id,
|
||||
activity_id=activity_id,
|
||||
coursechapter_id=coursechapter_id,
|
||||
name=name,
|
||||
type="documentpdf",
|
||||
course_id=coursechapter["course_id"],
|
||||
content={
|
||||
"documentpdf": {
|
||||
"filename": "documentpdf." + pdf_format,
|
||||
"activity_id": activity_id,
|
||||
}
|
||||
},
|
||||
creationDate=str(datetime.now()),
|
||||
updateDate=str(datetime.now()),
|
||||
)
|
||||
|
||||
await authorization_verify_based_on_roles(
|
||||
request,
|
||||
current_user.user_id,
|
||||
"create",
|
||||
user["roles"],
|
||||
activity_id,
|
||||
)
|
||||
|
||||
# create activity
|
||||
activity = ActivityInDB(**activity_object.dict())
|
||||
await activities.insert_one(activity.dict())
|
||||
|
||||
# upload pdf
|
||||
if pdf_file:
|
||||
# get pdffile format
|
||||
await upload_pdf(pdf_file, activity_id, org_id, coursechapter["course_id"])
|
||||
|
||||
# todo : choose whether to update the chapter or not
|
||||
# update chapter
|
||||
await courses.update_one(
|
||||
{"chapters_content.coursechapter_id": coursechapter_id},
|
||||
{"$addToSet": {"chapters_content.$.activities": activity_id}},
|
||||
)
|
||||
|
||||
return activity
|
||||
18
apps/api/src/services/courses/activities/uploads/pdfs.py
Normal file
18
apps/api/src/services/courses/activities/uploads/pdfs.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
|
||||
from src.services.utils.upload_content import upload_content
|
||||
|
||||
|
||||
async def upload_pdf(pdf_file, activity_id, org_id, course_id):
|
||||
contents = pdf_file.file.read()
|
||||
pdf_format = pdf_file.filename.split(".")[-1]
|
||||
|
||||
try:
|
||||
await upload_content(
|
||||
f"courses/{course_id}/activities/{activity_id}/documentpdf",
|
||||
org_id,
|
||||
contents,
|
||||
f"documentpdf.{pdf_format}",
|
||||
)
|
||||
|
||||
except Exception:
|
||||
return {"message": "There was an error uploading the file"}
|
||||
18
apps/api/src/services/courses/activities/uploads/videos.py
Normal file
18
apps/api/src/services/courses/activities/uploads/videos.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
|
||||
from src.services.utils.upload_content import upload_content
|
||||
|
||||
|
||||
async def upload_video(video_file, activity_id, org_id, course_id):
|
||||
contents = video_file.file.read()
|
||||
video_format = video_file.filename.split(".")[-1]
|
||||
|
||||
try:
|
||||
await upload_content(
|
||||
f"courses/{course_id}/activities/{activity_id}/video",
|
||||
org_id,
|
||||
contents,
|
||||
f"video.{video_format}",
|
||||
)
|
||||
|
||||
except Exception:
|
||||
return {"message": "There was an error uploading the file"}
|
||||
187
apps/api/src/services/courses/activities/video.py
Normal file
187
apps/api/src/services/courses/activities/video.py
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles,
|
||||
)
|
||||
from src.services.courses.activities.uploads.videos import upload_video
|
||||
from src.services.users.users import PublicUser
|
||||
from src.services.courses.activities.activities import ActivityInDB
|
||||
from fastapi import HTTPException, status, UploadFile, Request
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
async def create_video_activity(
|
||||
request: Request,
|
||||
name: str,
|
||||
coursechapter_id: str,
|
||||
current_user: PublicUser,
|
||||
video_file: UploadFile | None = None,
|
||||
):
|
||||
activities = request.app.db["activities"]
|
||||
courses = request.app.db["courses"]
|
||||
users = request.app.db["users"]
|
||||
|
||||
# get user
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
# generate activity_id
|
||||
activity_id = str(f"activity_{uuid4()}")
|
||||
|
||||
# get org_id from course
|
||||
coursechapter = await courses.find_one(
|
||||
{"chapters_content.coursechapter_id": coursechapter_id}
|
||||
)
|
||||
|
||||
if not coursechapter:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="CourseChapter : No coursechapter found",
|
||||
)
|
||||
|
||||
org_id = coursechapter["org_id"]
|
||||
|
||||
# check if video_file is not None
|
||||
if not video_file:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Video : No video file provided",
|
||||
)
|
||||
|
||||
if video_file.content_type not in ["video/mp4", "video/webm"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Video : Wrong video format"
|
||||
)
|
||||
|
||||
# get video format
|
||||
if video_file.filename:
|
||||
video_format = video_file.filename.split(".")[-1]
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Video : No video file provided",
|
||||
)
|
||||
|
||||
activity_object = ActivityInDB(
|
||||
org_id=org_id,
|
||||
activity_id=activity_id,
|
||||
coursechapter_id=coursechapter_id,
|
||||
course_id=coursechapter["course_id"],
|
||||
name=name,
|
||||
type="video",
|
||||
content={
|
||||
"video": {
|
||||
"filename": "video." + video_format,
|
||||
"activity_id": activity_id,
|
||||
}
|
||||
},
|
||||
creationDate=str(datetime.now()),
|
||||
updateDate=str(datetime.now()),
|
||||
)
|
||||
|
||||
await authorization_verify_based_on_roles(
|
||||
request,
|
||||
current_user.user_id,
|
||||
"create",
|
||||
user["roles"],
|
||||
activity_id,
|
||||
)
|
||||
|
||||
# create activity
|
||||
activity = ActivityInDB(**activity_object.dict())
|
||||
await activities.insert_one(activity.dict())
|
||||
|
||||
# upload video
|
||||
if video_file:
|
||||
# get videofile format
|
||||
await upload_video(video_file, activity_id, org_id, coursechapter["course_id"])
|
||||
|
||||
# todo : choose whether to update the chapter or not
|
||||
# update chapter
|
||||
await courses.update_one(
|
||||
{"chapters_content.coursechapter_id": coursechapter_id},
|
||||
{"$addToSet": {"chapters_content.$.activities": activity_id}},
|
||||
)
|
||||
|
||||
return activity
|
||||
|
||||
|
||||
class ExternalVideo(BaseModel):
|
||||
name: str
|
||||
uri: str
|
||||
type: Literal["youtube", "vimeo"]
|
||||
coursechapter_id: str
|
||||
|
||||
|
||||
class ExternalVideoInDB(BaseModel):
|
||||
activity_id: str
|
||||
|
||||
|
||||
async def create_external_video_activity(
|
||||
request: Request,
|
||||
current_user: PublicUser,
|
||||
data: ExternalVideo,
|
||||
):
|
||||
activities = request.app.db["activities"]
|
||||
courses = request.app.db["courses"]
|
||||
users = request.app.db["users"]
|
||||
|
||||
# get user
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
# generate activity_id
|
||||
activity_id = str(f"activity_{uuid4()}")
|
||||
|
||||
# get org_id from course
|
||||
coursechapter = await courses.find_one(
|
||||
{"chapters_content.coursechapter_id": data.coursechapter_id}
|
||||
)
|
||||
|
||||
if not coursechapter:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="CourseChapter : No coursechapter found",
|
||||
)
|
||||
|
||||
org_id = coursechapter["org_id"]
|
||||
|
||||
activity_object = ActivityInDB(
|
||||
org_id=org_id,
|
||||
activity_id=activity_id,
|
||||
coursechapter_id=data.coursechapter_id,
|
||||
name=data.name,
|
||||
type="video",
|
||||
content={
|
||||
"external_video": {
|
||||
"uri": data.uri,
|
||||
"activity_id": activity_id,
|
||||
"type": data.type,
|
||||
}
|
||||
},
|
||||
course_id=coursechapter["course_id"],
|
||||
creationDate=str(datetime.now()),
|
||||
updateDate=str(datetime.now()),
|
||||
)
|
||||
|
||||
await authorization_verify_based_on_roles(
|
||||
request,
|
||||
current_user.user_id,
|
||||
"create",
|
||||
user["roles"],
|
||||
activity_id,
|
||||
)
|
||||
|
||||
# create activity
|
||||
activity = ActivityInDB(**activity_object.dict())
|
||||
await activities.insert_one(activity.dict())
|
||||
|
||||
# todo : choose whether to update the chapter or not
|
||||
# update chapter
|
||||
await courses.update_one(
|
||||
{"chapters_content.coursechapter_id": data.coursechapter_id},
|
||||
{"$addToSet": {"chapters_content.$.activities": activity_id}},
|
||||
)
|
||||
|
||||
return activity
|
||||
367
apps/api/src/services/courses/chapters.py
Normal file
367
apps/api/src/services/courses/chapters.py
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
from datetime import datetime
|
||||
from typing import List, Literal
|
||||
from uuid import uuid4
|
||||
from pydantic import BaseModel
|
||||
from src.security.auth import non_public_endpoint
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles,
|
||||
authorization_verify_based_on_roles_and_authorship,
|
||||
authorization_verify_if_element_is_public,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
from src.services.courses.courses import Course
|
||||
from src.services.courses.activities.activities import ActivityInDB
|
||||
from src.services.users.users import PublicUser
|
||||
from fastapi import HTTPException, status, Request
|
||||
|
||||
|
||||
class CourseChapter(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
activities: list
|
||||
|
||||
|
||||
class CourseChapterInDB(CourseChapter):
|
||||
coursechapter_id: str
|
||||
course_id: str
|
||||
creationDate: str
|
||||
updateDate: str
|
||||
|
||||
|
||||
# Frontend
|
||||
class CourseChapterMetaData(BaseModel):
|
||||
chapterOrder: List[str]
|
||||
chapters: dict
|
||||
activities: object
|
||||
|
||||
|
||||
#### Classes ####################################################
|
||||
|
||||
####################################################
|
||||
# CRUD
|
||||
####################################################
|
||||
|
||||
|
||||
async def create_coursechapter(
|
||||
request: Request,
|
||||
coursechapter_object: CourseChapter,
|
||||
course_id: str,
|
||||
current_user: PublicUser,
|
||||
):
|
||||
courses = request.app.db["courses"]
|
||||
users = request.app.db["users"]
|
||||
# get course org_id and verify rights
|
||||
await courses.find_one({"course_id": course_id})
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
# generate coursechapter_id with uuid4
|
||||
coursechapter_id = str(f"coursechapter_{uuid4()}")
|
||||
|
||||
hasRoleRights = await authorization_verify_based_on_roles(
|
||||
request, current_user.user_id, "create", user["roles"], course_id
|
||||
)
|
||||
|
||||
if not hasRoleRights:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Roles : Insufficient rights to perform this action",
|
||||
)
|
||||
|
||||
coursechapter = CourseChapterInDB(
|
||||
coursechapter_id=coursechapter_id,
|
||||
creationDate=str(datetime.now()),
|
||||
updateDate=str(datetime.now()),
|
||||
course_id=course_id,
|
||||
**coursechapter_object.dict(),
|
||||
)
|
||||
|
||||
courses.update_one(
|
||||
{"course_id": course_id},
|
||||
{
|
||||
"$addToSet": {
|
||||
"chapters": coursechapter_id,
|
||||
"chapters_content": coursechapter.dict(),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return coursechapter.dict()
|
||||
|
||||
|
||||
async def get_coursechapter(
|
||||
request: Request, coursechapter_id: str, current_user: PublicUser
|
||||
):
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
coursechapter = await courses.find_one(
|
||||
{"chapters_content.coursechapter_id": coursechapter_id}
|
||||
)
|
||||
|
||||
if coursechapter:
|
||||
# verify course rights
|
||||
await verify_rights(request, coursechapter["course_id"], current_user, "read")
|
||||
coursechapter = CourseChapter(**coursechapter)
|
||||
|
||||
return coursechapter
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="CourseChapter does not exist"
|
||||
)
|
||||
|
||||
|
||||
async def update_coursechapter(
|
||||
request: Request,
|
||||
coursechapter_object: CourseChapter,
|
||||
coursechapter_id: str,
|
||||
current_user: PublicUser,
|
||||
):
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
coursechapter = await courses.find_one(
|
||||
{"chapters_content.coursechapter_id": coursechapter_id}
|
||||
)
|
||||
|
||||
if coursechapter:
|
||||
# verify course rights
|
||||
await verify_rights(request, coursechapter["course_id"], current_user, "update")
|
||||
|
||||
coursechapter = CourseChapterInDB(
|
||||
coursechapter_id=coursechapter_id,
|
||||
creationDate=str(datetime.now()),
|
||||
updateDate=str(datetime.now()),
|
||||
course_id=coursechapter["course_id"],
|
||||
**coursechapter_object.dict(),
|
||||
)
|
||||
|
||||
courses.update_one(
|
||||
{"chapters_content.coursechapter_id": coursechapter_id},
|
||||
{"$set": {"chapters_content.$": coursechapter.dict()}},
|
||||
)
|
||||
|
||||
return coursechapter
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Coursechapter does not exist"
|
||||
)
|
||||
|
||||
|
||||
async def delete_coursechapter(
|
||||
request: Request, coursechapter_id: str, current_user: PublicUser
|
||||
):
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
course = await courses.find_one(
|
||||
{"chapters_content.coursechapter_id": coursechapter_id}
|
||||
)
|
||||
|
||||
if course:
|
||||
# verify course rights
|
||||
await verify_rights(request, course["course_id"], current_user, "delete")
|
||||
|
||||
# Remove coursechapter from course
|
||||
await courses.update_one(
|
||||
{"course_id": course["course_id"]},
|
||||
{"$pull": {"chapters": coursechapter_id}},
|
||||
)
|
||||
|
||||
await courses.update_one(
|
||||
{"chapters_content.coursechapter_id": coursechapter_id},
|
||||
{"$pull": {"chapters_content": {"coursechapter_id": coursechapter_id}}},
|
||||
)
|
||||
|
||||
return {"message": "Coursechapter deleted"}
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
|
||||
)
|
||||
|
||||
|
||||
####################################################
|
||||
# Misc
|
||||
####################################################
|
||||
|
||||
|
||||
async def get_coursechapters(
|
||||
request: Request, course_id: str, page: int = 1, limit: int = 10
|
||||
):
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
course = await courses.find_one({"course_id": course_id})
|
||||
|
||||
if course:
|
||||
course = Course(**course)
|
||||
coursechapters = course.chapters_content
|
||||
|
||||
return coursechapters
|
||||
|
||||
|
||||
async def get_coursechapters_meta(
|
||||
request: Request, course_id: str, current_user: PublicUser
|
||||
):
|
||||
courses = request.app.db["courses"]
|
||||
activities = request.app.db["activities"]
|
||||
|
||||
await non_public_endpoint(current_user)
|
||||
|
||||
await verify_rights(request, course_id, current_user, "read")
|
||||
|
||||
coursechapters = await courses.find_one(
|
||||
{"course_id": course_id}, {"chapters": 1, "chapters_content": 1, "_id": 0}
|
||||
)
|
||||
|
||||
coursechapters = coursechapters
|
||||
|
||||
if not coursechapters:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
|
||||
)
|
||||
|
||||
# activities
|
||||
coursechapter_activityIds_global = []
|
||||
|
||||
# chapters
|
||||
chapters = {}
|
||||
if coursechapters["chapters_content"]:
|
||||
for coursechapter in coursechapters["chapters_content"]:
|
||||
coursechapter = CourseChapterInDB(**coursechapter)
|
||||
coursechapter_activityIds = []
|
||||
|
||||
for activity in coursechapter.activities:
|
||||
coursechapter_activityIds.append(activity)
|
||||
coursechapter_activityIds_global.append(activity)
|
||||
|
||||
chapters[coursechapter.coursechapter_id] = {
|
||||
"id": coursechapter.coursechapter_id,
|
||||
"name": coursechapter.name,
|
||||
"activityIds": coursechapter_activityIds,
|
||||
}
|
||||
|
||||
# activities
|
||||
activities_list = {}
|
||||
for activity in await activities.find(
|
||||
{"activity_id": {"$in": coursechapter_activityIds_global}}
|
||||
).to_list(length=100):
|
||||
activity = ActivityInDB(**activity)
|
||||
activities_list[activity.activity_id] = {
|
||||
"id": activity.activity_id,
|
||||
"name": activity.name,
|
||||
"type": activity.type,
|
||||
"content": activity.content,
|
||||
}
|
||||
|
||||
final = {
|
||||
"chapters": chapters,
|
||||
"chapterOrder": coursechapters["chapters"],
|
||||
"activities": activities_list,
|
||||
}
|
||||
|
||||
return final
|
||||
|
||||
|
||||
async def update_coursechapters_meta(
|
||||
request: Request,
|
||||
course_id: str,
|
||||
coursechapters_metadata: CourseChapterMetaData,
|
||||
current_user: PublicUser,
|
||||
):
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
await verify_rights(request, course_id, current_user, "update")
|
||||
|
||||
# update chapters in course
|
||||
await courses.update_one(
|
||||
{"course_id": course_id},
|
||||
{"$set": {"chapters": coursechapters_metadata.chapterOrder}},
|
||||
)
|
||||
|
||||
if coursechapters_metadata.chapters is not None:
|
||||
for (
|
||||
coursechapter_id,
|
||||
chapter_metadata,
|
||||
) in coursechapters_metadata.chapters.items():
|
||||
filter_query = {"chapters_content.coursechapter_id": coursechapter_id}
|
||||
update_query = {
|
||||
"$set": {
|
||||
"chapters_content.$.activities": chapter_metadata["activityIds"]
|
||||
}
|
||||
}
|
||||
result = await courses.update_one(filter_query, update_query)
|
||||
if result.matched_count == 0:
|
||||
# handle error when no documents are matched by the filter query
|
||||
print(f"No documents found for course chapter ID {coursechapter_id}")
|
||||
|
||||
# update activities in coursechapters
|
||||
activity = request.app.db["activities"]
|
||||
if coursechapters_metadata.chapters is not None:
|
||||
for (
|
||||
coursechapter_id,
|
||||
chapter_metadata,
|
||||
) in coursechapters_metadata.chapters.items():
|
||||
# Update coursechapter_id in activities
|
||||
filter_query = {"activity_id": {"$in": chapter_metadata["activityIds"]}}
|
||||
update_query = {"$set": {"coursechapter_id": coursechapter_id}}
|
||||
|
||||
result = await activity.update_many(filter_query, update_query)
|
||||
if result.matched_count == 0:
|
||||
# handle error when no documents are matched by the filter query
|
||||
print(f"No documents found for course chapter ID {coursechapter_id}")
|
||||
|
||||
return {"detail": "coursechapters metadata updated"}
|
||||
|
||||
|
||||
#### Security ####################################################
|
||||
|
||||
|
||||
async def verify_rights(
|
||||
request: Request,
|
||||
course_id: str,
|
||||
current_user: PublicUser,
|
||||
action: Literal["read", "update", "delete"],
|
||||
):
|
||||
courses = request.app.db["courses"]
|
||||
users = request.app.db["users"]
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
course = await courses.find_one({"course_id": course_id})
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
|
||||
)
|
||||
|
||||
if action == "read":
|
||||
if current_user.user_id == "anonymous":
|
||||
await authorization_verify_if_element_is_public(
|
||||
request, course_id, current_user.user_id, action
|
||||
)
|
||||
else:
|
||||
users = request.app.db["users"]
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
await authorization_verify_if_user_is_anon(current_user.user_id)
|
||||
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request,
|
||||
current_user.user_id,
|
||||
action,
|
||||
user["roles"],
|
||||
course_id,
|
||||
)
|
||||
else:
|
||||
users = request.app.db["users"]
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
await authorization_verify_if_user_is_anon(current_user.user_id)
|
||||
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request,
|
||||
current_user.user_id,
|
||||
action,
|
||||
user["roles"],
|
||||
course_id,
|
||||
)
|
||||
|
||||
|
||||
#### Security ####################################################
|
||||
242
apps/api/src/services/courses/collections.py
Normal file
242
apps/api/src/services/courses/collections.py
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
from typing import List, Literal
|
||||
from uuid import uuid4
|
||||
from pydantic import BaseModel
|
||||
from src.security.rbac.rbac import authorization_verify_based_on_roles_and_authorship, authorization_verify_if_user_is_anon
|
||||
from src.services.users.users import PublicUser
|
||||
from fastapi import HTTPException, status, Request
|
||||
|
||||
#### Classes ####################################################
|
||||
|
||||
|
||||
class Collection(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
courses: List[str] # course_id
|
||||
public: bool
|
||||
org_id: str # org_id
|
||||
|
||||
|
||||
class CollectionInDB(Collection):
|
||||
collection_id: str
|
||||
authors: List[str] # user_id
|
||||
|
||||
|
||||
#### Classes ####################################################
|
||||
|
||||
####################################################
|
||||
# CRUD
|
||||
####################################################
|
||||
|
||||
|
||||
async def get_collection(
|
||||
request: Request, collection_id: str, current_user: PublicUser
|
||||
):
|
||||
collections = request.app.db["collections"]
|
||||
|
||||
collection = await collections.find_one({"collection_id": collection_id})
|
||||
|
||||
# verify collection rights
|
||||
await verify_collection_rights(
|
||||
request, collection_id, current_user, "read", collection["org_id"]
|
||||
)
|
||||
|
||||
if not collection:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
|
||||
)
|
||||
|
||||
collection = Collection(**collection)
|
||||
|
||||
# add courses to collection
|
||||
courses = request.app.db["courses"]
|
||||
courseids = [course for course in collection.courses]
|
||||
|
||||
collection.courses = []
|
||||
collection.courses = courses.find({"course_id": {"$in": courseids}}, {"_id": 0})
|
||||
|
||||
collection.courses = [
|
||||
course for course in await collection.courses.to_list(length=100)
|
||||
]
|
||||
|
||||
return collection
|
||||
|
||||
|
||||
async def create_collection(
|
||||
request: Request, collection_object: Collection, current_user: PublicUser
|
||||
):
|
||||
collections = request.app.db["collections"]
|
||||
|
||||
# find if collection already exists using name
|
||||
isCollectionNameAvailable = await collections.find_one(
|
||||
{"name": collection_object.name}
|
||||
)
|
||||
|
||||
# TODO
|
||||
# await verify_collection_rights("*", current_user, "create")
|
||||
|
||||
if isCollectionNameAvailable:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Collection name already exists",
|
||||
)
|
||||
|
||||
# generate collection_id with uuid4
|
||||
collection_id = str(f"collection_{uuid4()}")
|
||||
|
||||
collection = CollectionInDB(
|
||||
collection_id=collection_id,
|
||||
authors=[current_user.user_id],
|
||||
**collection_object.dict(),
|
||||
)
|
||||
|
||||
collection_in_db = await collections.insert_one(collection.dict())
|
||||
|
||||
if not collection_in_db:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Unavailable database",
|
||||
)
|
||||
|
||||
return collection.dict()
|
||||
|
||||
|
||||
async def update_collection(
|
||||
request: Request,
|
||||
collection_object: Collection,
|
||||
collection_id: str,
|
||||
current_user: PublicUser,
|
||||
):
|
||||
# verify collection rights
|
||||
|
||||
collections = request.app.db["collections"]
|
||||
|
||||
collection = await collections.find_one({"collection_id": collection_id})
|
||||
|
||||
await verify_collection_rights(
|
||||
request, collection_id, current_user, "update", collection["org_id"]
|
||||
)
|
||||
|
||||
if not collection:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
|
||||
)
|
||||
|
||||
updated_collection = CollectionInDB(
|
||||
collection_id=collection_id, **collection_object.dict()
|
||||
)
|
||||
|
||||
await collections.update_one(
|
||||
{"collection_id": collection_id}, {"$set": updated_collection.dict()}
|
||||
)
|
||||
|
||||
return Collection(**updated_collection.dict())
|
||||
|
||||
|
||||
async def delete_collection(
|
||||
request: Request, collection_id: str, current_user: PublicUser
|
||||
):
|
||||
collections = request.app.db["collections"]
|
||||
|
||||
collection = await collections.find_one({"collection_id": collection_id})
|
||||
|
||||
await verify_collection_rights(
|
||||
request, collection_id, current_user, "delete", collection["org_id"]
|
||||
)
|
||||
|
||||
if not collection:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
|
||||
)
|
||||
|
||||
isDeleted = await collections.delete_one({"collection_id": collection_id})
|
||||
|
||||
if isDeleted:
|
||||
return {"detail": "collection deleted"}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Unavailable database",
|
||||
)
|
||||
|
||||
|
||||
####################################################
|
||||
# Misc
|
||||
####################################################
|
||||
|
||||
|
||||
async def get_collections(
|
||||
request: Request,
|
||||
org_id: str,
|
||||
current_user: PublicUser,
|
||||
page: int = 1,
|
||||
limit: int = 10,
|
||||
):
|
||||
collections = request.app.db["collections"]
|
||||
|
||||
|
||||
if current_user.user_id == "anonymous":
|
||||
all_collections = collections.find(
|
||||
{"org_id": org_id, "public": True}, {"_id": 0}
|
||||
)
|
||||
else:
|
||||
# get all collections from database without ObjectId
|
||||
all_collections = (
|
||||
collections.find({"org_id": org_id})
|
||||
.sort("name", 1)
|
||||
.skip(10 * (page - 1))
|
||||
.limit(limit)
|
||||
)
|
||||
|
||||
# create list of collections and include courses in each collection
|
||||
collections_list = []
|
||||
for collection in await all_collections.to_list(length=100):
|
||||
collection = CollectionInDB(**collection)
|
||||
collections_list.append(collection)
|
||||
|
||||
collection_courses = [course for course in collection.courses]
|
||||
# add courses to collection
|
||||
courses = request.app.db["courses"]
|
||||
collection.courses = []
|
||||
collection.courses = courses.find(
|
||||
{"course_id": {"$in": collection_courses}}, {"_id": 0}
|
||||
)
|
||||
|
||||
collection.courses = [
|
||||
course for course in await collection.courses.to_list(length=100)
|
||||
]
|
||||
|
||||
return collections_list
|
||||
|
||||
|
||||
#### Security ####################################################
|
||||
|
||||
|
||||
async def verify_collection_rights(
|
||||
request: Request,
|
||||
collection_id: str,
|
||||
current_user: PublicUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
org_id: str,
|
||||
):
|
||||
collections = request.app.db["collections"]
|
||||
users = request.app.db["users"]
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
collection = await collections.find_one({"collection_id": collection_id})
|
||||
|
||||
if not collection and action != "create" and collection_id != "*":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
|
||||
)
|
||||
|
||||
# Collections are public by default for now
|
||||
if current_user.user_id == "anonymous" and action == "read":
|
||||
return True
|
||||
|
||||
await authorization_verify_if_user_is_anon(current_user.user_id)
|
||||
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.user_id, action, user["roles"], collection_id
|
||||
)
|
||||
|
||||
|
||||
#### Security ####################################################
|
||||
413
apps/api/src/services/courses/courses.py
Normal file
413
apps/api/src/services/courses/courses.py
Normal file
|
|
@ -0,0 +1,413 @@
|
|||
import json
|
||||
from typing import List, Literal, Optional
|
||||
from uuid import uuid4
|
||||
from pydantic import BaseModel
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles,
|
||||
authorization_verify_based_on_roles_and_authorship,
|
||||
authorization_verify_if_element_is_public,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
from src.services.courses.activities.activities import ActivityInDB
|
||||
from src.services.courses.thumbnails import upload_thumbnail
|
||||
from src.services.users.schemas.users import AnonymousUser
|
||||
from src.services.users.users import PublicUser
|
||||
from fastapi import HTTPException, Request, status, UploadFile
|
||||
from datetime import datetime
|
||||
|
||||
#### Classes ####################################################
|
||||
|
||||
|
||||
class Course(BaseModel):
|
||||
name: str
|
||||
mini_description: str
|
||||
description: str
|
||||
learnings: List[str]
|
||||
thumbnail: str
|
||||
public: bool
|
||||
chapters: List[str]
|
||||
chapters_content: Optional[List]
|
||||
org_id: str
|
||||
|
||||
|
||||
class CourseInDB(Course):
|
||||
course_id: str
|
||||
creationDate: str
|
||||
updateDate: str
|
||||
authors: List[str]
|
||||
|
||||
|
||||
# TODO : wow terrible, fix this
|
||||
# those models need to be available only in the chapters service
|
||||
class CourseChapter(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
activities: list
|
||||
|
||||
|
||||
class CourseChapterInDB(CourseChapter):
|
||||
coursechapter_id: str
|
||||
course_id: str
|
||||
creationDate: str
|
||||
updateDate: str
|
||||
|
||||
|
||||
#### Classes ####################################################
|
||||
|
||||
# TODO : Add courses photo & cover upload and delete
|
||||
|
||||
|
||||
####################################################
|
||||
# CRUD
|
||||
####################################################
|
||||
|
||||
|
||||
async def get_course(request: Request, course_id: str, current_user: PublicUser):
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
course = await courses.find_one({"course_id": course_id})
|
||||
|
||||
# verify course rights
|
||||
await verify_rights(request, course_id, current_user, "read")
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
|
||||
)
|
||||
|
||||
course = Course(**course)
|
||||
return course
|
||||
|
||||
|
||||
async def get_course_meta(request: Request, course_id: str, current_user: PublicUser):
|
||||
courses = request.app.db["courses"]
|
||||
trails = request.app.db["trails"]
|
||||
|
||||
course = await courses.find_one({"course_id": course_id})
|
||||
activities = request.app.db["activities"]
|
||||
|
||||
# verify course rights
|
||||
await verify_rights(request, course_id, current_user, "read")
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
|
||||
)
|
||||
|
||||
coursechapters = await courses.find_one(
|
||||
{"course_id": course_id}, {"chapters_content": 1, "_id": 0}
|
||||
)
|
||||
|
||||
# activities
|
||||
coursechapter_activityIds_global = []
|
||||
|
||||
# chapters
|
||||
chapters = {}
|
||||
if coursechapters["chapters_content"]:
|
||||
for coursechapter in coursechapters["chapters_content"]:
|
||||
coursechapter = CourseChapterInDB(**coursechapter)
|
||||
coursechapter_activityIds = []
|
||||
|
||||
for activity in coursechapter.activities:
|
||||
coursechapter_activityIds.append(activity)
|
||||
coursechapter_activityIds_global.append(activity)
|
||||
|
||||
chapters[coursechapter.coursechapter_id] = {
|
||||
"id": coursechapter.coursechapter_id,
|
||||
"name": coursechapter.name,
|
||||
"activityIds": coursechapter_activityIds,
|
||||
}
|
||||
|
||||
# activities
|
||||
activities_list = {}
|
||||
for activity in await activities.find(
|
||||
{"activity_id": {"$in": coursechapter_activityIds_global}}
|
||||
).to_list(length=100):
|
||||
activity = ActivityInDB(**activity)
|
||||
activities_list[activity.activity_id] = {
|
||||
"id": activity.activity_id,
|
||||
"name": activity.name,
|
||||
"type": activity.type,
|
||||
"content": activity.content,
|
||||
}
|
||||
|
||||
chapters_list_with_activities = []
|
||||
for chapter in chapters:
|
||||
chapters_list_with_activities.append(
|
||||
{
|
||||
"id": chapters[chapter]["id"],
|
||||
"name": chapters[chapter]["name"],
|
||||
"activities": [
|
||||
activities_list[activity]
|
||||
for activity in chapters[chapter]["activityIds"]
|
||||
],
|
||||
}
|
||||
)
|
||||
course = CourseInDB(**course)
|
||||
|
||||
# Get activity by user
|
||||
trail = await trails.find_one(
|
||||
{"courses.course_id": course_id, "user_id": current_user.user_id}
|
||||
)
|
||||
if trail:
|
||||
# get only the course where course_id == course_id
|
||||
trail_course = next(
|
||||
(course for course in trail["courses"] if course["course_id"] == course_id),
|
||||
None,
|
||||
)
|
||||
else:
|
||||
trail_course = ""
|
||||
|
||||
return {
|
||||
"course": course,
|
||||
"chapters": chapters_list_with_activities,
|
||||
"trail": trail_course,
|
||||
}
|
||||
|
||||
|
||||
async def create_course(
|
||||
request: Request,
|
||||
course_object: Course,
|
||||
org_id: str,
|
||||
current_user: PublicUser,
|
||||
thumbnail_file: UploadFile | None = None,
|
||||
):
|
||||
courses = request.app.db["courses"]
|
||||
users = request.app.db["users"]
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
# generate course_id with uuid4
|
||||
course_id = str(f"course_{uuid4()}")
|
||||
|
||||
# TODO(fix) : the implementation here is clearly not the best one (this entire function)
|
||||
course_object.org_id = org_id
|
||||
course_object.chapters_content = []
|
||||
|
||||
await authorization_verify_based_on_roles(
|
||||
request,
|
||||
current_user.user_id,
|
||||
"create",
|
||||
user["roles"],
|
||||
course_id,
|
||||
)
|
||||
|
||||
|
||||
if thumbnail_file and thumbnail_file.filename:
|
||||
name_in_disk = (
|
||||
f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
|
||||
)
|
||||
await upload_thumbnail(
|
||||
thumbnail_file, name_in_disk, course_object.org_id, course_id
|
||||
)
|
||||
course_object.thumbnail = name_in_disk
|
||||
|
||||
course = CourseInDB(
|
||||
course_id=course_id,
|
||||
authors=[current_user.user_id],
|
||||
creationDate=str(datetime.now()),
|
||||
updateDate=str(datetime.now()),
|
||||
**course_object.dict(),
|
||||
)
|
||||
|
||||
course_in_db = await courses.insert_one(course.dict())
|
||||
|
||||
if not course_in_db:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Unavailable database",
|
||||
)
|
||||
|
||||
return course.dict()
|
||||
|
||||
|
||||
async def update_course_thumbnail(
|
||||
request: Request,
|
||||
course_id: str,
|
||||
current_user: PublicUser,
|
||||
thumbnail_file: UploadFile | None = None,
|
||||
):
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
course = await courses.find_one({"course_id": course_id})
|
||||
|
||||
# verify course rights
|
||||
await verify_rights(request, course_id, current_user, "update")
|
||||
|
||||
# TODO(fix) : the implementation here is clearly not the best one
|
||||
if course:
|
||||
creationDate = course["creationDate"]
|
||||
authors = course["authors"]
|
||||
if thumbnail_file and thumbnail_file.filename:
|
||||
name_in_disk = f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
|
||||
course = Course(**course).copy(update={"thumbnail": name_in_disk})
|
||||
await upload_thumbnail(
|
||||
thumbnail_file, name_in_disk, course.org_id, course_id
|
||||
)
|
||||
|
||||
updated_course = CourseInDB(
|
||||
course_id=course_id,
|
||||
creationDate=creationDate,
|
||||
authors=authors,
|
||||
updateDate=str(datetime.now()),
|
||||
**course.dict(),
|
||||
)
|
||||
|
||||
await courses.update_one(
|
||||
{"course_id": course_id}, {"$set": updated_course.dict()}
|
||||
)
|
||||
|
||||
return CourseInDB(**updated_course.dict())
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
|
||||
)
|
||||
|
||||
|
||||
async def update_course(
|
||||
request: Request, course_object: Course, course_id: str, current_user: PublicUser
|
||||
):
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
course = await courses.find_one({"course_id": course_id})
|
||||
|
||||
# verify course rights
|
||||
await verify_rights(request, course_id, current_user, "update")
|
||||
|
||||
if course:
|
||||
creationDate = course["creationDate"]
|
||||
authors = course["authors"]
|
||||
|
||||
# get today's date
|
||||
datetime_object = datetime.now()
|
||||
|
||||
updated_course = CourseInDB(
|
||||
course_id=course_id,
|
||||
creationDate=creationDate,
|
||||
authors=authors,
|
||||
updateDate=str(datetime_object),
|
||||
**course_object.dict(),
|
||||
)
|
||||
|
||||
await courses.update_one(
|
||||
{"course_id": course_id}, {"$set": updated_course.dict()}
|
||||
)
|
||||
|
||||
return CourseInDB(**updated_course.dict())
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
|
||||
)
|
||||
|
||||
|
||||
async def delete_course(request: Request, course_id: str, current_user: PublicUser):
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
course = await courses.find_one({"course_id": course_id})
|
||||
|
||||
# verify course rights
|
||||
await verify_rights(request, course_id, current_user, "delete")
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
|
||||
)
|
||||
|
||||
isDeleted = await courses.delete_one({"course_id": course_id})
|
||||
|
||||
if isDeleted:
|
||||
return {"detail": "Course deleted"}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Unavailable database",
|
||||
)
|
||||
|
||||
|
||||
####################################################
|
||||
# Misc
|
||||
####################################################
|
||||
|
||||
|
||||
async def get_courses_orgslug(
|
||||
request: Request,
|
||||
current_user: PublicUser,
|
||||
page: int = 1,
|
||||
limit: int = 10,
|
||||
org_slug: str | None = None,
|
||||
):
|
||||
courses = request.app.db["courses"]
|
||||
orgs = request.app.db["organizations"]
|
||||
|
||||
# get org_id from slug
|
||||
org = await orgs.find_one({"slug": org_slug})
|
||||
|
||||
if not org:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist"
|
||||
)
|
||||
|
||||
# show only public courses if user is not logged in
|
||||
if current_user.user_id == "anonymous":
|
||||
all_courses = (
|
||||
courses.find({"org_id": org["org_id"], "public": True})
|
||||
.sort("name", 1)
|
||||
.skip(10 * (page - 1))
|
||||
.limit(limit)
|
||||
)
|
||||
else:
|
||||
all_courses = (
|
||||
courses.find({"org_id": org["org_id"]})
|
||||
.sort("name", 1)
|
||||
.skip(10 * (page - 1))
|
||||
.limit(limit)
|
||||
)
|
||||
|
||||
return [
|
||||
json.loads(json.dumps(course, default=str))
|
||||
for course in await all_courses.to_list(length=100)
|
||||
]
|
||||
|
||||
|
||||
#### Security ####################################################
|
||||
|
||||
|
||||
async def verify_rights(
|
||||
request: Request,
|
||||
course_id: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
):
|
||||
if action == "read":
|
||||
if current_user.user_id == "anonymous":
|
||||
await authorization_verify_if_element_is_public(
|
||||
request, course_id, current_user.user_id, action
|
||||
)
|
||||
else:
|
||||
users = request.app.db["users"]
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request,
|
||||
current_user.user_id,
|
||||
action,
|
||||
user["roles"],
|
||||
course_id,
|
||||
)
|
||||
else:
|
||||
users = request.app.db["users"]
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
await authorization_verify_if_user_is_anon(current_user.user_id)
|
||||
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request,
|
||||
current_user.user_id,
|
||||
action,
|
||||
user["roles"],
|
||||
course_id,
|
||||
)
|
||||
|
||||
|
||||
#### Security ####################################################
|
||||
16
apps/api/src/services/courses/thumbnails.py
Normal file
16
apps/api/src/services/courses/thumbnails.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
|
||||
from src.services.utils.upload_content import upload_content
|
||||
|
||||
|
||||
async def upload_thumbnail(thumbnail_file, name_in_disk, org_id, course_id):
|
||||
contents = thumbnail_file.file.read()
|
||||
try:
|
||||
await upload_content(
|
||||
f"courses/{course_id}/thumbnails",
|
||||
org_id,
|
||||
contents,
|
||||
f"{name_in_disk}",
|
||||
)
|
||||
|
||||
except Exception:
|
||||
return {"message": "There was an error uploading the file"}
|
||||
0
apps/api/src/services/dev/__init__.py
Normal file
0
apps/api/src/services/dev/__init__.py
Normal file
18
apps/api/src/services/dev/dev.py
Normal file
18
apps/api/src/services/dev/dev.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
from fastapi import HTTPException
|
||||
from config.config import get_learnhouse_config
|
||||
|
||||
|
||||
def isDevModeEnabled():
|
||||
config = get_learnhouse_config()
|
||||
if config.general_config.development_mode:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def isDevModeEnabledOrRaise():
|
||||
config = get_learnhouse_config()
|
||||
if config.general_config.development_mode:
|
||||
return True
|
||||
else:
|
||||
raise HTTPException(status_code=403, detail="Development mode is disabled")
|
||||
0
apps/api/src/services/dev/mocks/__init__.py
Normal file
0
apps/api/src/services/dev/mocks/__init__.py
Normal file
214
apps/api/src/services/dev/mocks/initial.py
Normal file
214
apps/api/src/services/dev/mocks/initial.py
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
import os
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
from fastapi import Request
|
||||
from src.security.security import security_hash_password
|
||||
from src.services.courses.chapters import CourseChapter, create_coursechapter
|
||||
from src.services.courses.activities.activities import Activity, create_activity
|
||||
from src.services.users.users import PublicUser, UserInDB
|
||||
|
||||
from src.services.orgs.orgs import Organization, create_org
|
||||
from src.services.roles.schemas.roles import Permission, Elements, RoleInDB
|
||||
from src.services.courses.courses import CourseInDB
|
||||
from faker import Faker
|
||||
|
||||
|
||||
async def create_initial_data(request: Request):
|
||||
fake = Faker(['en_US'])
|
||||
fake_multilang = Faker(
|
||||
['en_US', 'de_DE', 'ja_JP', 'es_ES', 'it_IT', 'pt_BR', 'ar_PS'])
|
||||
|
||||
|
||||
# Create users
|
||||
########################################
|
||||
|
||||
database_users = request.app.db["users"]
|
||||
await database_users.delete_many({})
|
||||
|
||||
users = []
|
||||
admin_user = UserInDB(
|
||||
user_id="user_admin",
|
||||
creation_date=str(datetime.now()),
|
||||
update_date=str(datetime.now()),
|
||||
roles= [],
|
||||
orgs=[],
|
||||
username="admin",
|
||||
email="admin@admin.admin",
|
||||
password=str(await security_hash_password("admin")),
|
||||
)
|
||||
|
||||
await database_users.insert_one(admin_user.dict())
|
||||
|
||||
# find admin user
|
||||
users = request.app.db["users"]
|
||||
admin_user = await users.find_one({"username": "admin"})
|
||||
|
||||
if admin_user:
|
||||
admin_user = UserInDB(**admin_user)
|
||||
current_user = PublicUser(**admin_user.dict())
|
||||
else:
|
||||
raise Exception("Admin user not found")
|
||||
# Create roles
|
||||
########################################
|
||||
|
||||
database_roles = request.app.db["roles"]
|
||||
await database_roles.delete_many({})
|
||||
|
||||
|
||||
roles = []
|
||||
admin_role = RoleInDB(
|
||||
name="Admin",
|
||||
description="Admin",
|
||||
elements=Elements(
|
||||
courses=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
users=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
houses=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
collections=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
organizations=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
coursechapters=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
activities=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
),
|
||||
org_id="org_test",
|
||||
role_id="role_admin",
|
||||
created_at=str(datetime.now()),
|
||||
updated_at=str(datetime.now()),
|
||||
)
|
||||
|
||||
roles.append(admin_role)
|
||||
|
||||
for role in roles:
|
||||
database_roles.insert_one(role.dict())
|
||||
|
||||
|
||||
# Create organizations
|
||||
########################################
|
||||
|
||||
database_orgs = request.app.db["organizations"]
|
||||
await database_orgs.delete_many({})
|
||||
|
||||
organizations = []
|
||||
for i in range(0, 2):
|
||||
company = fake.company()
|
||||
# remove whitespace and special characters and make lowercase
|
||||
slug = ''.join(e for e in company if e.isalnum()).lower()
|
||||
org = Organization(
|
||||
name=company,
|
||||
description=fake.unique.text(),
|
||||
email=fake.unique.email(),
|
||||
slug=slug,
|
||||
logo="",
|
||||
default=False
|
||||
)
|
||||
organizations.append(org)
|
||||
await create_org(request, org, current_user)
|
||||
|
||||
|
||||
# Generate Courses and CourseChapters
|
||||
########################################
|
||||
|
||||
database_courses = request.app.db["courses"]
|
||||
await database_courses.delete_many({})
|
||||
|
||||
courses = []
|
||||
orgs = request.app.db["organizations"]
|
||||
|
||||
if await orgs.count_documents({}) > 0:
|
||||
for org in await orgs.find().to_list(length=100):
|
||||
for i in range(0, 5):
|
||||
|
||||
# get image in BinaryIO format from unsplash and save it to disk
|
||||
image = requests.get(
|
||||
"https://source.unsplash.com/random/800x600")
|
||||
with open("thumbnail.jpg", "wb") as f:
|
||||
f.write(image.content)
|
||||
|
||||
course_id = f"course_{uuid4()}"
|
||||
course = CourseInDB(
|
||||
name=fake_multilang.unique.sentence(),
|
||||
description=fake_multilang.unique.text(),
|
||||
mini_description=fake_multilang.unique.text(),
|
||||
thumbnail="thumbnail",
|
||||
org_id=org['org_id'],
|
||||
learnings=[fake_multilang.unique.sentence()
|
||||
for i in range(0, 5)],
|
||||
public=True,
|
||||
chapters=[],
|
||||
course_id=course_id,
|
||||
creationDate=str(datetime.now()),
|
||||
updateDate=str(datetime.now()),
|
||||
authors=[current_user.user_id],
|
||||
chapters_content=[],
|
||||
)
|
||||
|
||||
courses = request.app.db["courses"]
|
||||
name_in_disk = f"test_mock{course_id}.jpeg"
|
||||
|
||||
image = requests.get(
|
||||
"https://source.unsplash.com/random/800x600/?img=1")
|
||||
|
||||
# check if folder exists and create it if not
|
||||
if not os.path.exists("content/uploads/img"):
|
||||
|
||||
os.makedirs("content/uploads/img")
|
||||
|
||||
with open(f"content/uploads/img/{name_in_disk}", "wb") as f:
|
||||
f.write(image.content)
|
||||
|
||||
course.thumbnail = name_in_disk
|
||||
|
||||
course = CourseInDB(**course.dict())
|
||||
await courses.insert_one(course.dict())
|
||||
|
||||
# create chapters
|
||||
for i in range(0, 5):
|
||||
coursechapter = CourseChapter(
|
||||
name=fake_multilang.unique.sentence(),
|
||||
description=fake_multilang.unique.text(),
|
||||
activities=[],
|
||||
)
|
||||
coursechapter = await create_coursechapter(request,coursechapter, course_id, current_user)
|
||||
if coursechapter:
|
||||
# create activities
|
||||
for i in range(0, 5):
|
||||
activity = Activity(
|
||||
name=fake_multilang.unique.sentence(),
|
||||
type="dynamic",
|
||||
content={},
|
||||
)
|
||||
activity = await create_activity(request,activity, "org_test", coursechapter['coursechapter_id'], current_user)
|
||||
0
apps/api/src/services/install/__init__.py
Normal file
0
apps/api/src/services/install/__init__.py
Normal file
419
apps/api/src/services/install/install.py
Normal file
419
apps/api/src/services/install/install.py
Normal file
|
|
@ -0,0 +1,419 @@
|
|||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
from fastapi import HTTPException, Request, status
|
||||
from pydantic import BaseModel
|
||||
import requests
|
||||
from config.config import get_learnhouse_config
|
||||
from src.security.security import security_hash_password
|
||||
from src.services.courses.activities.activities import Activity, create_activity
|
||||
from src.services.courses.chapters import create_coursechapter, CourseChapter
|
||||
from src.services.courses.courses import CourseInDB
|
||||
|
||||
from src.services.orgs.schemas.orgs import Organization, OrganizationInDB
|
||||
from faker import Faker
|
||||
|
||||
|
||||
from src.services.roles.schemas.roles import Elements, Permission, RoleInDB
|
||||
from src.services.users.schemas.users import (
|
||||
PublicUser,
|
||||
User,
|
||||
UserInDB,
|
||||
UserOrganization,
|
||||
UserRolesInOrganization,
|
||||
UserWithPassword,
|
||||
)
|
||||
|
||||
|
||||
class InstallInstance(BaseModel):
|
||||
install_id: str
|
||||
created_date: str
|
||||
updated_date: str
|
||||
step: int
|
||||
data: dict
|
||||
|
||||
|
||||
async def isInstallModeEnabled():
|
||||
config = get_learnhouse_config()
|
||||
|
||||
if config.general_config.install_mode:
|
||||
return True
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Install mode is not enabled",
|
||||
)
|
||||
|
||||
|
||||
async def create_install_instance(request: Request, data: dict):
|
||||
installs = request.app.db["installs"]
|
||||
|
||||
# get install_id
|
||||
install_id = str(f"install_{uuid4()}")
|
||||
created_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
updated_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
step = 1
|
||||
|
||||
# create install
|
||||
install = InstallInstance(
|
||||
install_id=install_id,
|
||||
created_date=created_date,
|
||||
updated_date=updated_date,
|
||||
step=step,
|
||||
data=data,
|
||||
)
|
||||
|
||||
# insert install
|
||||
installs.insert_one(install.dict())
|
||||
|
||||
return install
|
||||
|
||||
|
||||
async def get_latest_install_instance(request: Request):
|
||||
installs = request.app.db["installs"]
|
||||
|
||||
# get latest created install instance using find_one
|
||||
install = await installs.find_one(
|
||||
sort=[("created_date", -1)], limit=1, projection={"_id": 0}
|
||||
)
|
||||
|
||||
if install is None:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="No install instance found",
|
||||
)
|
||||
|
||||
else:
|
||||
install = InstallInstance(**install)
|
||||
|
||||
return install
|
||||
|
||||
|
||||
async def update_install_instance(request: Request, data: dict, step: int):
|
||||
installs = request.app.db["installs"]
|
||||
|
||||
# get latest created install
|
||||
install = await installs.find_one(
|
||||
sort=[("created_date", -1)], limit=1, projection={"_id": 0}
|
||||
)
|
||||
|
||||
if install is None:
|
||||
return None
|
||||
|
||||
else:
|
||||
# update install
|
||||
install["data"] = data
|
||||
install["step"] = step
|
||||
install["updated_date"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# update install
|
||||
await installs.update_one(
|
||||
{"install_id": install["install_id"]}, {"$set": install}
|
||||
)
|
||||
|
||||
install = InstallInstance(**install)
|
||||
|
||||
return install
|
||||
|
||||
|
||||
############################################################################################################
|
||||
# Steps
|
||||
############################################################################################################
|
||||
|
||||
|
||||
# Install Default roles
|
||||
async def install_default_elements(request: Request, data: dict):
|
||||
roles = request.app.db["roles"]
|
||||
|
||||
# check if default roles ADMIN_ROLE and USER_ROLE already exist
|
||||
admin_role = await roles.find_one({"role_id": "role_admin"})
|
||||
user_role = await roles.find_one({"role_id": "role_member"})
|
||||
|
||||
if admin_role is not None or user_role is not None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Default roles already exist",
|
||||
)
|
||||
|
||||
# get default roles
|
||||
ADMIN_ROLE = RoleInDB(
|
||||
name="Admin Role",
|
||||
description="This role grants all permissions to the user",
|
||||
elements=Elements(
|
||||
courses=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
users=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
houses=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
collections=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
organizations=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
coursechapters=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
activities=Permission(
|
||||
action_create=True,
|
||||
action_read=True,
|
||||
action_update=True,
|
||||
action_delete=True,
|
||||
),
|
||||
),
|
||||
org_id="*",
|
||||
role_id="role_admin",
|
||||
created_at=str(datetime.now()),
|
||||
updated_at=str(datetime.now()),
|
||||
)
|
||||
|
||||
USER_ROLE = RoleInDB(
|
||||
name="Member Role",
|
||||
description="This role grants read-only permissions to the user",
|
||||
elements=Elements(
|
||||
courses=Permission(
|
||||
action_create=False,
|
||||
action_read=True,
|
||||
action_update=False,
|
||||
action_delete=False,
|
||||
),
|
||||
users=Permission(
|
||||
action_create=False,
|
||||
action_read=True,
|
||||
action_update=False,
|
||||
action_delete=False,
|
||||
),
|
||||
houses=Permission(
|
||||
action_create=False,
|
||||
action_read=True,
|
||||
action_update=False,
|
||||
action_delete=False,
|
||||
),
|
||||
collections=Permission(
|
||||
action_create=False,
|
||||
action_read=True,
|
||||
action_update=False,
|
||||
action_delete=False,
|
||||
),
|
||||
organizations=Permission(
|
||||
action_create=False,
|
||||
action_read=True,
|
||||
action_update=False,
|
||||
action_delete=False,
|
||||
),
|
||||
coursechapters=Permission(
|
||||
action_create=False,
|
||||
action_read=True,
|
||||
action_update=False,
|
||||
action_delete=False,
|
||||
),
|
||||
activities=Permission(
|
||||
action_create=False,
|
||||
action_read=True,
|
||||
action_update=False,
|
||||
action_delete=False,
|
||||
),
|
||||
),
|
||||
org_id="*",
|
||||
role_id="role_member",
|
||||
created_at=str(datetime.now()),
|
||||
updated_at=str(datetime.now()),
|
||||
)
|
||||
|
||||
try:
|
||||
# insert default roles
|
||||
await roles.insert_many([USER_ROLE.dict(), ADMIN_ROLE.dict()])
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Error while inserting default roles",
|
||||
)
|
||||
|
||||
|
||||
# Organization creation
|
||||
async def install_create_organization(
|
||||
request: Request,
|
||||
org_object: Organization,
|
||||
):
|
||||
orgs = request.app.db["organizations"]
|
||||
request.app.db["users"]
|
||||
|
||||
# find if org already exists using name
|
||||
|
||||
isOrgAvailable = await orgs.find_one({"slug": org_object.slug.lower()})
|
||||
|
||||
if isOrgAvailable:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Organization slug already exists",
|
||||
)
|
||||
|
||||
# generate org_id with uuid4
|
||||
org_id = str(f"org_{uuid4()}")
|
||||
|
||||
org = OrganizationInDB(org_id=org_id, **org_object.dict())
|
||||
|
||||
org_in_db = await orgs.insert_one(org.dict())
|
||||
|
||||
if not org_in_db:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Unavailable database",
|
||||
)
|
||||
|
||||
return org.dict()
|
||||
|
||||
|
||||
async def install_create_organization_user(
|
||||
request: Request, user_object: UserWithPassword, org_slug: str
|
||||
):
|
||||
users = request.app.db["users"]
|
||||
|
||||
isUsernameAvailable = await users.find_one({"username": user_object.username})
|
||||
isEmailAvailable = await users.find_one({"email": user_object.email})
|
||||
|
||||
if isUsernameAvailable:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Username already exists"
|
||||
)
|
||||
|
||||
if isEmailAvailable:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Email already exists"
|
||||
)
|
||||
|
||||
# Generate user_id with uuid4
|
||||
user_id = str(f"user_{uuid4()}")
|
||||
|
||||
# Set the username & hash the password
|
||||
user_object.username = user_object.username.lower()
|
||||
user_object.password = await security_hash_password(user_object.password)
|
||||
|
||||
# Get org_id from org_slug
|
||||
orgs = request.app.db["organizations"]
|
||||
|
||||
# Check if the org exists
|
||||
isOrgExists = await orgs.find_one({"slug": org_slug})
|
||||
|
||||
# If the org does not exist, raise an error
|
||||
if not isOrgExists:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="You are trying to create a user in an organization that does not exist",
|
||||
)
|
||||
|
||||
org_id = isOrgExists["org_id"]
|
||||
|
||||
# Create initial orgs list with the org_id passed in
|
||||
orgs = [UserOrganization(org_id=org_id, org_role="owner")]
|
||||
|
||||
# Give role
|
||||
roles = [UserRolesInOrganization(role_id="role_admin", org_id=org_id)]
|
||||
|
||||
# Create the user
|
||||
user = UserInDB(
|
||||
user_id=user_id,
|
||||
creation_date=str(datetime.now()),
|
||||
update_date=str(datetime.now()),
|
||||
orgs=orgs,
|
||||
roles=roles,
|
||||
**user_object.dict(),
|
||||
)
|
||||
|
||||
# Insert the user into the database
|
||||
await users.insert_one(user.dict())
|
||||
|
||||
return User(**user.dict())
|
||||
|
||||
|
||||
async def create_sample_data(org_slug: str, username: str, request: Request):
|
||||
Faker(["en_US"])
|
||||
fake_multilang = Faker(
|
||||
["en_US", "de_DE", "ja_JP", "es_ES", "it_IT", "pt_BR", "ar_PS"]
|
||||
)
|
||||
|
||||
users = request.app.db["users"]
|
||||
orgs = request.app.db["organizations"]
|
||||
user = await users.find_one({"username": username})
|
||||
org = await orgs.find_one({"slug": org_slug.lower()})
|
||||
user_id = user["user_id"]
|
||||
org_id = org["org_id"]
|
||||
|
||||
current_user = PublicUser(**user)
|
||||
|
||||
for i in range(0, 5):
|
||||
# get image in BinaryIO format from unsplash and save it to disk
|
||||
image = requests.get("https://source.unsplash.com/random/800x600")
|
||||
with open("thumbnail.jpg", "wb") as f:
|
||||
f.write(image.content)
|
||||
|
||||
course_id = f"course_{uuid4()}"
|
||||
course = CourseInDB(
|
||||
name=fake_multilang.unique.sentence(),
|
||||
description=fake_multilang.unique.text(),
|
||||
mini_description=fake_multilang.unique.text(),
|
||||
thumbnail="thumbnail",
|
||||
org_id=org_id,
|
||||
learnings=[fake_multilang.unique.sentence() for i in range(0, 5)],
|
||||
public=True,
|
||||
chapters=[],
|
||||
course_id=course_id,
|
||||
creationDate=str(datetime.now()),
|
||||
updateDate=str(datetime.now()),
|
||||
authors=[user_id],
|
||||
chapters_content=[],
|
||||
)
|
||||
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
course = CourseInDB(**course.dict())
|
||||
await courses.insert_one(course.dict())
|
||||
|
||||
# create chapters
|
||||
for i in range(0, 5):
|
||||
coursechapter = CourseChapter(
|
||||
name=fake_multilang.unique.sentence(),
|
||||
description=fake_multilang.unique.text(),
|
||||
activities=[],
|
||||
)
|
||||
coursechapter = await create_coursechapter(
|
||||
request, coursechapter, course_id, current_user
|
||||
)
|
||||
if coursechapter:
|
||||
# create activities
|
||||
for i in range(0, 5):
|
||||
activity = Activity(
|
||||
name=fake_multilang.unique.sentence(),
|
||||
type="dynamic",
|
||||
content={},
|
||||
)
|
||||
activity = await create_activity(
|
||||
request,
|
||||
activity,
|
||||
org_id,
|
||||
coursechapter["coursechapter_id"],
|
||||
current_user,
|
||||
)
|
||||
0
apps/api/src/services/orgs/__init__.py
Normal file
0
apps/api/src/services/orgs/__init__.py
Normal file
17
apps/api/src/services/orgs/logos.py
Normal file
17
apps/api/src/services/orgs/logos.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from uuid import uuid4
|
||||
|
||||
from src.services.utils.upload_content import upload_content
|
||||
|
||||
|
||||
async def upload_org_logo(logo_file, org_id):
|
||||
contents = logo_file.file.read()
|
||||
name_in_disk = f"{uuid4()}.{logo_file.filename.split('.')[-1]}"
|
||||
|
||||
await upload_content(
|
||||
"logos",
|
||||
org_id,
|
||||
contents,
|
||||
name_in_disk,
|
||||
)
|
||||
|
||||
return name_in_disk
|
||||
225
apps/api/src/services/orgs/orgs.py
Normal file
225
apps/api/src/services/orgs/orgs.py
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
import json
|
||||
from typing import Literal
|
||||
from uuid import uuid4
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
from src.services.orgs.logos import upload_org_logo
|
||||
from src.services.orgs.schemas.orgs import (
|
||||
Organization,
|
||||
OrganizationInDB,
|
||||
PublicOrganization,
|
||||
)
|
||||
from src.services.users.schemas.users import UserOrganization
|
||||
from src.services.users.users import PublicUser
|
||||
from fastapi import HTTPException, UploadFile, status, Request
|
||||
|
||||
|
||||
async def get_organization(request: Request, org_id: str):
|
||||
orgs = request.app.db["organizations"]
|
||||
|
||||
org = await orgs.find_one({"org_id": org_id})
|
||||
|
||||
if not org:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist"
|
||||
)
|
||||
|
||||
org = PublicOrganization(**org)
|
||||
return org
|
||||
|
||||
|
||||
async def get_organization_by_slug(request: Request, org_slug: str):
|
||||
orgs = request.app.db["organizations"]
|
||||
|
||||
org = await orgs.find_one({"slug": org_slug})
|
||||
|
||||
if not org:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist"
|
||||
)
|
||||
|
||||
org = PublicOrganization(**org)
|
||||
return org
|
||||
|
||||
|
||||
async def create_org(
|
||||
request: Request, org_object: Organization, current_user: PublicUser
|
||||
):
|
||||
orgs = request.app.db["organizations"]
|
||||
user = request.app.db["users"]
|
||||
|
||||
# find if org already exists using name
|
||||
isOrgAvailable = await orgs.find_one({"slug": org_object.slug})
|
||||
|
||||
if isOrgAvailable:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Organization slug already exists",
|
||||
)
|
||||
|
||||
# generate org_id with uuid4
|
||||
org_id = str(f"org_{uuid4()}")
|
||||
|
||||
org = OrganizationInDB(org_id=org_id, **org_object.dict())
|
||||
|
||||
org_in_db = await orgs.insert_one(org.dict())
|
||||
|
||||
user_organization: UserOrganization = UserOrganization(
|
||||
org_id=org_id, org_role="owner"
|
||||
)
|
||||
|
||||
# add org to user
|
||||
await user.update_one(
|
||||
{"user_id": current_user.user_id},
|
||||
{"$addToSet": {"orgs": user_organization.dict()}},
|
||||
)
|
||||
|
||||
# add role admin to org
|
||||
await user.update_one(
|
||||
{"user_id": current_user.user_id},
|
||||
{"$addToSet": {"roles": {"org_id": org_id, "role_id": "role_admin"}}},
|
||||
)
|
||||
|
||||
if not org_in_db:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Unavailable database",
|
||||
)
|
||||
|
||||
return org.dict()
|
||||
|
||||
|
||||
async def update_org(
|
||||
request: Request, org_object: Organization, org_id: str, current_user: PublicUser
|
||||
):
|
||||
# verify org rights
|
||||
await verify_org_rights(request, org_id, current_user, "update")
|
||||
|
||||
orgs = request.app.db["organizations"]
|
||||
|
||||
await orgs.find_one({"org_id": org_id})
|
||||
|
||||
updated_org = OrganizationInDB(org_id=org_id, **org_object.dict())
|
||||
|
||||
# update org
|
||||
await orgs.update_one({"org_id": org_id}, {"$set": updated_org.dict()})
|
||||
|
||||
return updated_org.dict()
|
||||
|
||||
|
||||
async def update_org_logo(
|
||||
request: Request, logo_file: UploadFile, org_id: str, current_user: PublicUser
|
||||
):
|
||||
# verify org rights
|
||||
await verify_org_rights(request, org_id, current_user, "update")
|
||||
|
||||
orgs = request.app.db["organizations"]
|
||||
|
||||
await orgs.find_one({"org_id": org_id})
|
||||
|
||||
name_in_disk = await upload_org_logo(logo_file, org_id)
|
||||
|
||||
# update org
|
||||
await orgs.update_one({"org_id": org_id}, {"$set": {"logo": name_in_disk}})
|
||||
|
||||
return {"detail": "Logo updated"}
|
||||
|
||||
|
||||
async def delete_org(request: Request, org_id: str, current_user: PublicUser):
|
||||
await verify_org_rights(request, org_id, current_user, "delete")
|
||||
|
||||
orgs = request.app.db["organizations"]
|
||||
|
||||
org = await orgs.find_one({"org_id": org_id})
|
||||
|
||||
if not org:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist"
|
||||
)
|
||||
|
||||
isDeleted = await orgs.delete_one({"org_id": org_id})
|
||||
|
||||
# remove org from all users
|
||||
users = request.app.db["users"]
|
||||
await users.update_many({}, {"$pull": {"orgs": {"org_id": org_id}}})
|
||||
|
||||
if isDeleted:
|
||||
return {"detail": "Org deleted"}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Unavailable database",
|
||||
)
|
||||
|
||||
|
||||
async def get_orgs_by_user(
|
||||
request: Request, user_id: str, page: int = 1, limit: int = 10
|
||||
):
|
||||
orgs = request.app.db["organizations"]
|
||||
user = request.app.db["users"]
|
||||
|
||||
if user_id == "anonymous":
|
||||
# raise error
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="User not logged in"
|
||||
)
|
||||
|
||||
# get user orgs
|
||||
user_orgs = await user.find_one({"user_id": user_id})
|
||||
|
||||
org_ids: list[UserOrganization] = []
|
||||
|
||||
for org in user_orgs["orgs"]:
|
||||
if (
|
||||
org["org_role"] == "owner"
|
||||
or org["org_role"] == "editor"
|
||||
or org["org_role"] == "member"
|
||||
):
|
||||
org_ids.append(org["org_id"])
|
||||
|
||||
# find all orgs where org_id is in org_ids array
|
||||
|
||||
all_orgs = (
|
||||
orgs.find({"org_id": {"$in": org_ids}})
|
||||
.sort("name", 1)
|
||||
.skip(10 * (page - 1))
|
||||
.limit(100)
|
||||
)
|
||||
|
||||
return [
|
||||
json.loads(json.dumps(org, default=str))
|
||||
for org in await all_orgs.to_list(length=100)
|
||||
]
|
||||
|
||||
|
||||
#### Security ####################################################
|
||||
|
||||
|
||||
async def verify_org_rights(
|
||||
request: Request,
|
||||
org_id: str,
|
||||
current_user: PublicUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
):
|
||||
orgs = request.app.db["organizations"]
|
||||
users = request.app.db["users"]
|
||||
|
||||
user = await users.find_one({"user_id": current_user.user_id})
|
||||
|
||||
org = await orgs.find_one({"org_id": org_id})
|
||||
|
||||
if not org:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist"
|
||||
)
|
||||
|
||||
await authorization_verify_if_user_is_anon(current_user.user_id)
|
||||
|
||||
await authorization_verify_based_on_roles(
|
||||
request, current_user.user_id, action, user["roles"], org_id
|
||||
)
|
||||
|
||||
|
||||
#### Security ####################################################
|
||||
0
apps/api/src/services/orgs/schemas/__init__.py
Normal file
0
apps/api/src/services/orgs/schemas/__init__.py
Normal file
28
apps/api/src/services/orgs/schemas/orgs.py
Normal file
28
apps/api/src/services/orgs/schemas/orgs.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
#### Classes ####################################################
|
||||
|
||||
|
||||
class Organization(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
email: str
|
||||
slug: str
|
||||
logo: Optional[str]
|
||||
default: Optional[bool] = False
|
||||
|
||||
|
||||
class OrganizationInDB(Organization):
|
||||
org_id: str
|
||||
|
||||
|
||||
class PublicOrganization(Organization):
|
||||
name: str
|
||||
description: str
|
||||
email: str
|
||||
slug: str
|
||||
org_id: str
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
0
apps/api/src/services/roles/__init__.py
Normal file
0
apps/api/src/services/roles/__init__.py
Normal file
127
apps/api/src/services/roles/roles.py
Normal file
127
apps/api/src/services/roles/roles.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
from typing import Literal
|
||||
from uuid import uuid4
|
||||
from src.security.rbac.rbac import authorization_verify_if_user_is_anon
|
||||
from src.services.roles.schemas.roles import Role, RoleInDB
|
||||
from src.services.users.schemas.users import PublicUser
|
||||
from fastapi import HTTPException, status, Request
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
async def create_role(request: Request, role_object: Role, current_user: PublicUser):
|
||||
roles = request.app.db["roles"]
|
||||
|
||||
await verify_user_permissions_on_roles(request, current_user, "create", None)
|
||||
|
||||
# create the role object in the database and return the object
|
||||
role_id = "role_" + str(uuid4())
|
||||
|
||||
role = RoleInDB(
|
||||
role_id=role_id,
|
||||
created_at=str(datetime.now()),
|
||||
updated_at=str(datetime.now()),
|
||||
**role_object.dict()
|
||||
)
|
||||
|
||||
await roles.insert_one(role.dict())
|
||||
|
||||
return role
|
||||
|
||||
|
||||
async def read_role(request: Request, role_id: str, current_user: PublicUser):
|
||||
roles = request.app.db["roles"]
|
||||
|
||||
await verify_user_permissions_on_roles(request, current_user, "read", role_id)
|
||||
|
||||
role = RoleInDB(**await roles.find_one({"role_id": role_id}))
|
||||
|
||||
return role
|
||||
|
||||
|
||||
async def update_role(
|
||||
request: Request, role_id: str, role_object: Role, current_user: PublicUser
|
||||
):
|
||||
roles = request.app.db["roles"]
|
||||
|
||||
await verify_user_permissions_on_roles(request, current_user, "update", role_id)
|
||||
|
||||
role_object.updated_at = datetime.now()
|
||||
|
||||
# Update the role object in the database and return the object
|
||||
updated_role = RoleInDB(
|
||||
**await roles.find_one_and_update(
|
||||
{"role_id": role_id}, {"$set": role_object.dict()}, return_document=True
|
||||
)
|
||||
)
|
||||
|
||||
return updated_role
|
||||
|
||||
|
||||
async def delete_role(request: Request, role_id: str, current_user: PublicUser):
|
||||
roles = request.app.db["roles"]
|
||||
|
||||
await verify_user_permissions_on_roles(request, current_user, "delete", role_id)
|
||||
|
||||
# Delete the role object in the database and return the object
|
||||
deleted_role = RoleInDB(**await roles.find_one_and_delete({"role_id": role_id}))
|
||||
|
||||
return deleted_role
|
||||
|
||||
|
||||
#### Security ####################################################
|
||||
|
||||
|
||||
async def verify_user_permissions_on_roles(
|
||||
request: Request,
|
||||
current_user: PublicUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
role_id: str | None,
|
||||
):
|
||||
request.app.db["users"]
|
||||
roles = request.app.db["roles"]
|
||||
|
||||
# If current user is not authenticated
|
||||
|
||||
if not current_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail="Roles : Not authenticated"
|
||||
)
|
||||
|
||||
await authorization_verify_if_user_is_anon(current_user.user_id)
|
||||
|
||||
if action == "create":
|
||||
if "owner" in [org.org_role for org in current_user.orgs]:
|
||||
return True
|
||||
|
||||
if role_id is not None:
|
||||
role = RoleInDB(**await roles.find_one({"role_id": role_id}))
|
||||
|
||||
if action == "read":
|
||||
if "owner" in [org.org_role for org in current_user.orgs]:
|
||||
return True
|
||||
|
||||
for org in current_user.orgs:
|
||||
if org.org_id == role.org_id:
|
||||
return True
|
||||
|
||||
if action == "update":
|
||||
for org in current_user.orgs:
|
||||
# If the user is an owner of the organization
|
||||
if org.org_id == role.org_id:
|
||||
if org.org_role == "owner" or org.org_role == "editor":
|
||||
return True
|
||||
# Can't update a global role
|
||||
if role.org_id == "*":
|
||||
return False
|
||||
|
||||
if action == "delete":
|
||||
for org in current_user.orgs:
|
||||
# If the user is an owner of the organization
|
||||
if org.org_id == role.org_id:
|
||||
if org.org_role == "owner":
|
||||
return True
|
||||
# Can't delete a global role
|
||||
if role.org_id == "*":
|
||||
return False
|
||||
|
||||
|
||||
#### Security ####################################################
|
||||
0
apps/api/src/services/roles/schemas/__init__.py
Normal file
0
apps/api/src/services/roles/schemas/__init__.py
Normal file
41
apps/api/src/services/roles/schemas/roles.py
Normal file
41
apps/api/src/services/roles/schemas/roles.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
from typing import Literal
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
# Database Models
|
||||
|
||||
class Permission(BaseModel):
|
||||
action_create: bool
|
||||
action_read: bool
|
||||
action_update: bool
|
||||
action_delete: bool
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
|
||||
|
||||
class Elements(BaseModel):
|
||||
courses: Permission
|
||||
users: Permission
|
||||
houses: Permission
|
||||
collections: Permission
|
||||
organizations: Permission
|
||||
coursechapters: Permission
|
||||
activities: Permission
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
|
||||
|
||||
class Role(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
elements : Elements
|
||||
org_id: str | Literal["*"]
|
||||
|
||||
|
||||
class RoleInDB(Role):
|
||||
role_id: str
|
||||
created_at: str
|
||||
updated_at: str
|
||||
|
||||
0
apps/api/src/services/trail/__init__.py
Normal file
0
apps/api/src/services/trail/__init__.py
Normal file
286
apps/api/src/services/trail/trail.py
Normal file
286
apps/api/src/services/trail/trail.py
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
from datetime import datetime
|
||||
from typing import List, Literal, Optional
|
||||
from uuid import uuid4
|
||||
from fastapi import HTTPException, Request, status
|
||||
from pydantic import BaseModel
|
||||
from src.services.courses.chapters import get_coursechapters_meta
|
||||
from src.services.orgs.orgs import PublicOrganization
|
||||
|
||||
from src.services.users.users import PublicUser
|
||||
|
||||
#### Classes ####################################################
|
||||
|
||||
|
||||
class ActivityData(BaseModel):
|
||||
activity_id: str
|
||||
activity_type: str
|
||||
data: Optional[dict]
|
||||
|
||||
|
||||
class TrailCourse(BaseModel):
|
||||
course_id: str
|
||||
elements_type: Optional[Literal["course"]] = "course"
|
||||
status: Optional[Literal["ongoing", "done", "closed"]] = "ongoing"
|
||||
course_object: dict
|
||||
masked: Optional[bool] = False
|
||||
activities_marked_complete: Optional[List[str]]
|
||||
activities_data: Optional[List[ActivityData]]
|
||||
progress: Optional[int]
|
||||
|
||||
|
||||
class Trail(BaseModel):
|
||||
status: Optional[Literal["ongoing", "done", "closed"]] = "ongoing"
|
||||
masked: Optional[bool] = False
|
||||
courses: Optional[List[TrailCourse]]
|
||||
|
||||
|
||||
class TrailInDB(Trail):
|
||||
trail_id: str
|
||||
org_id: str
|
||||
user_id: str
|
||||
creationDate: str = datetime.now().isoformat()
|
||||
updateDate: str = datetime.now().isoformat()
|
||||
|
||||
|
||||
#### Classes ####################################################
|
||||
|
||||
|
||||
async def create_trail(
|
||||
request: Request, user: PublicUser, org_id: str, trail_object: Trail
|
||||
) -> Trail:
|
||||
trails = request.app.db["trails"]
|
||||
|
||||
# get list of courses
|
||||
if trail_object.courses:
|
||||
courses = trail_object.courses
|
||||
# get course ids
|
||||
course_ids = [course.course_id for course in courses]
|
||||
|
||||
# find if the user has already started the course
|
||||
existing_trail = await trails.find_one(
|
||||
{"user_id": user.user_id, "courses.course_id": {"$in": course_ids}}
|
||||
)
|
||||
if existing_trail:
|
||||
# update the status of the element with the matching course_id to "ongoing"
|
||||
for element in existing_trail["courses"]:
|
||||
if element["course_id"] in course_ids:
|
||||
element["status"] = "ongoing"
|
||||
# update the existing trail in the database
|
||||
await trails.replace_one(
|
||||
{"trail_id": existing_trail["trail_id"]}, existing_trail
|
||||
)
|
||||
|
||||
# create trail id
|
||||
trail_id = f"trail_{uuid4()}"
|
||||
|
||||
# create trail
|
||||
trail = TrailInDB(
|
||||
**trail_object.dict(), trail_id=trail_id, user_id=user.user_id, org_id=org_id
|
||||
)
|
||||
|
||||
await trails.insert_one(trail.dict())
|
||||
|
||||
return trail
|
||||
|
||||
|
||||
async def get_user_trail(request: Request, org_slug: str, user: PublicUser) -> Trail:
|
||||
trails = request.app.db["trails"]
|
||||
trail = await trails.find_one({"user_id": user.user_id})
|
||||
if not trail:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found"
|
||||
)
|
||||
for element in trail["courses"]:
|
||||
course_id = element["course_id"]
|
||||
chapters_meta = await get_coursechapters_meta(request, course_id, user)
|
||||
activities = chapters_meta["activities"]
|
||||
num_activities = len(activities)
|
||||
|
||||
num_completed_activities = len(element.get("activities_marked_complete", []))
|
||||
element["progress"] = (
|
||||
round((num_completed_activities / num_activities) * 100, 2)
|
||||
if num_activities > 0
|
||||
else 0
|
||||
)
|
||||
|
||||
return Trail(**trail)
|
||||
|
||||
|
||||
async def get_user_trail_with_orgslug(
|
||||
request: Request, user: PublicUser, org_slug: str
|
||||
) -> Trail:
|
||||
trails = request.app.db["trails"]
|
||||
orgs = request.app.db["organizations"]
|
||||
courses_mongo = request.app.db["courses"]
|
||||
|
||||
# get org_id from orgslug
|
||||
org = await orgs.find_one({"slug": org_slug})
|
||||
|
||||
trail = await trails.find_one({"user_id": user.user_id, "org_id": org["org_id"]})
|
||||
|
||||
if not trail:
|
||||
return Trail(masked=False, courses=[])
|
||||
|
||||
course_ids = [course["course_id"] for course in trail["courses"]]
|
||||
|
||||
live_courses = await courses_mongo.find({"course_id": {"$in": course_ids}}).to_list(
|
||||
length=None
|
||||
)
|
||||
|
||||
for course in trail["courses"]:
|
||||
course_id = course["course_id"]
|
||||
|
||||
if course_id not in [course["course_id"] for course in live_courses]:
|
||||
course["masked"] = True
|
||||
continue
|
||||
|
||||
chapters_meta = await get_coursechapters_meta(request, course_id, user)
|
||||
activities = chapters_meta["activities"]
|
||||
|
||||
# get course object without _id
|
||||
course_object = await courses_mongo.find_one(
|
||||
{"course_id": course_id}, {"_id": 0}
|
||||
)
|
||||
|
||||
course["course_object"] = course_object
|
||||
num_activities = len(activities)
|
||||
|
||||
num_completed_activities = len(course.get("activities_marked_complete", []))
|
||||
course["progress"] = (
|
||||
round((num_completed_activities / num_activities) * 100, 2)
|
||||
if num_activities > 0
|
||||
else 0
|
||||
)
|
||||
|
||||
return Trail(**trail)
|
||||
|
||||
|
||||
async def add_activity_to_trail(
|
||||
request: Request, user: PublicUser, course_id: str, org_slug: str, activity_id: str
|
||||
) -> Trail:
|
||||
trails = request.app.db["trails"]
|
||||
orgs = request.app.db["organizations"]
|
||||
courseid = "course_" + course_id
|
||||
activityid = "activity_" + activity_id
|
||||
|
||||
# get org_id from orgslug
|
||||
org = await orgs.find_one({"slug": org_slug})
|
||||
org_id = org["org_id"]
|
||||
|
||||
# find a trail with the user_id and course_id in the courses array
|
||||
trail = await trails.find_one(
|
||||
{"user_id": user.user_id, "courses.course_id": courseid, "org_id": org_id}
|
||||
)
|
||||
|
||||
if user.user_id == "anonymous":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Anonymous users cannot add activity to trail",
|
||||
)
|
||||
|
||||
if not trail:
|
||||
return Trail(masked=False, courses=[])
|
||||
|
||||
# if a trail has course_id in the courses array, then add the activity_id to the activities_marked_complete array
|
||||
for element in trail["courses"]:
|
||||
if element["course_id"] == courseid:
|
||||
if "activities_marked_complete" in element:
|
||||
# check if activity_id is already in the array
|
||||
if activityid not in element["activities_marked_complete"]:
|
||||
element["activities_marked_complete"].append(activityid)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Activity already marked complete",
|
||||
)
|
||||
else:
|
||||
element["activities_marked_complete"] = [activity_id]
|
||||
|
||||
# modify trail object
|
||||
await trails.replace_one({"trail_id": trail["trail_id"]}, trail)
|
||||
|
||||
return Trail(**trail)
|
||||
|
||||
|
||||
async def add_course_to_trail(
|
||||
request: Request, user: PublicUser, orgslug: str, course_id: str
|
||||
) -> Trail:
|
||||
trails = request.app.db["trails"]
|
||||
orgs = request.app.db["organizations"]
|
||||
|
||||
if user.user_id == "anonymous":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Anonymous users cannot add activity to trail",
|
||||
)
|
||||
|
||||
org = await orgs.find_one({"slug": orgslug})
|
||||
|
||||
org = PublicOrganization(**org)
|
||||
|
||||
trail = await trails.find_one({"user_id": user.user_id, "org_id": org["org_id"]})
|
||||
|
||||
if not trail:
|
||||
trail_to_insert = TrailInDB(
|
||||
trail_id=f"trail_{uuid4()}",
|
||||
user_id=user.user_id,
|
||||
org_id=org["org_id"],
|
||||
courses=[],
|
||||
)
|
||||
trail_to_insert = await trails.insert_one(trail_to_insert.dict())
|
||||
|
||||
trail = await trails.find_one({"_id": trail_to_insert.inserted_id})
|
||||
|
||||
# check if course is already present in the trail
|
||||
for element in trail["courses"]:
|
||||
if element["course_id"] == course_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Course already present in the trail",
|
||||
)
|
||||
|
||||
updated_trail = TrailCourse(
|
||||
course_id=course_id,
|
||||
activities_data=[],
|
||||
activities_marked_complete=[],
|
||||
progress=0,
|
||||
course_object={},
|
||||
status="ongoing",
|
||||
masked=False,
|
||||
)
|
||||
trail["courses"].append(updated_trail.dict())
|
||||
await trails.replace_one({"trail_id": trail["trail_id"]}, trail)
|
||||
return Trail(**trail)
|
||||
|
||||
|
||||
async def remove_course_from_trail(
|
||||
request: Request, user: PublicUser, orgslug: str, course_id: str
|
||||
) -> Trail:
|
||||
trails = request.app.db["trails"]
|
||||
orgs = request.app.db["organizations"]
|
||||
|
||||
if user.user_id == "anonymous":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Anonymous users cannot add activity to trail",
|
||||
)
|
||||
|
||||
org = await orgs.find_one({"slug": orgslug})
|
||||
|
||||
org = PublicOrganization(**org)
|
||||
trail = await trails.find_one({"user_id": user.user_id, "org_id": org["org_id"]})
|
||||
|
||||
if not trail:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found"
|
||||
)
|
||||
|
||||
# check if course is already present in the trail
|
||||
|
||||
for element in trail["courses"]:
|
||||
if element["course_id"] == course_id:
|
||||
trail["courses"].remove(element)
|
||||
break
|
||||
|
||||
await trails.replace_one({"trail_id": trail["trail_id"]}, trail)
|
||||
return Trail(**trail)
|
||||
0
apps/api/src/services/users/__init__.py
Normal file
0
apps/api/src/services/users/__init__.py
Normal file
0
apps/api/src/services/users/schemas/__init__.py
Normal file
0
apps/api/src/services/users/schemas/__init__.py
Normal file
70
apps/api/src/services/users/schemas/users.py
Normal file
70
apps/api/src/services/users/schemas/users.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
from typing import Literal
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class UserOrganization(BaseModel):
|
||||
org_id: str
|
||||
org_role: Literal['owner', 'editor', 'member']
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
|
||||
class UserRolesInOrganization(BaseModel):
|
||||
org_id: str
|
||||
role_id: str
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
email: str
|
||||
full_name: str | None = None
|
||||
avatar_url: str | None = None
|
||||
bio: str | None = None
|
||||
|
||||
|
||||
|
||||
class UserWithPassword(User):
|
||||
password: str
|
||||
|
||||
|
||||
class UserInDB(User):
|
||||
user_id: str
|
||||
password: str
|
||||
verified: bool | None = False
|
||||
disabled: bool | None = False
|
||||
orgs: list[UserOrganization] = []
|
||||
roles: list[UserRolesInOrganization] = []
|
||||
creation_date: str
|
||||
update_date: str
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
|
||||
|
||||
|
||||
|
||||
class PublicUser(User):
|
||||
user_id: str
|
||||
orgs: list[UserOrganization] = []
|
||||
roles: list[UserRolesInOrganization] = []
|
||||
creation_date: str
|
||||
update_date: str
|
||||
|
||||
class AnonymousUser(BaseModel):
|
||||
user_id: str = "anonymous"
|
||||
username: str = "anonymous"
|
||||
roles: list[UserRolesInOrganization] = [
|
||||
UserRolesInOrganization(org_id="anonymous", role_id="role_anonymous")
|
||||
]
|
||||
|
||||
|
||||
|
||||
# Forms ####################################################
|
||||
|
||||
class PasswordChangeForm(BaseModel):
|
||||
old_password: str
|
||||
new_password: str
|
||||
321
apps/api/src/services/users/users.py
Normal file
321
apps/api/src/services/users/users.py
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
from datetime import datetime
|
||||
from typing import Literal
|
||||
from uuid import uuid4
|
||||
from fastapi import HTTPException, Request, status
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
from src.security.security import security_hash_password, security_verify_password
|
||||
from src.services.users.schemas.users import (
|
||||
PasswordChangeForm,
|
||||
PublicUser,
|
||||
User,
|
||||
UserOrganization,
|
||||
UserRolesInOrganization,
|
||||
UserWithPassword,
|
||||
UserInDB,
|
||||
)
|
||||
|
||||
|
||||
async def create_user(
|
||||
request: Request,
|
||||
current_user: PublicUser | None,
|
||||
user_object: UserWithPassword,
|
||||
org_slug: str,
|
||||
):
|
||||
users = request.app.db["users"]
|
||||
|
||||
isUsernameAvailable = await users.find_one({"username": user_object.username})
|
||||
isEmailAvailable = await users.find_one({"email": user_object.email})
|
||||
|
||||
if isUsernameAvailable:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Username already exists"
|
||||
)
|
||||
|
||||
if isEmailAvailable:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Email already exists"
|
||||
)
|
||||
|
||||
# Generate user_id with uuid4
|
||||
user_id = str(f"user_{uuid4()}")
|
||||
|
||||
# Check if the requesting user is authenticated
|
||||
if current_user is not None:
|
||||
# Verify rights
|
||||
await verify_user_rights_on_user(request, current_user, "create", user_id)
|
||||
|
||||
# Set the username & hash the password
|
||||
user_object.username = user_object.username.lower()
|
||||
user_object.password = await security_hash_password(user_object.password)
|
||||
|
||||
# Get org_id from org_slug
|
||||
orgs = request.app.db["organizations"]
|
||||
|
||||
# Check if the org exists
|
||||
isOrgExists = await orgs.find_one({"slug": org_slug})
|
||||
|
||||
# If the org does not exist, raise an error
|
||||
if not isOrgExists and (org_slug != "None"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="You are trying to create a user in an organization that does not exist",
|
||||
)
|
||||
|
||||
org_id = isOrgExists["org_id"] if org_slug != "None" else ''
|
||||
|
||||
# Create initial orgs list with the org_id passed in
|
||||
orgs = (
|
||||
[UserOrganization(org_id=org_id, org_role="member")]
|
||||
if org_slug != "None"
|
||||
else []
|
||||
)
|
||||
|
||||
# Give role
|
||||
roles = (
|
||||
[UserRolesInOrganization(role_id="role_member", org_id=org_id)]
|
||||
if org_slug != "None"
|
||||
else []
|
||||
)
|
||||
|
||||
# Create the user
|
||||
user = UserInDB(
|
||||
user_id=user_id,
|
||||
creation_date=str(datetime.now()),
|
||||
update_date=str(datetime.now()),
|
||||
orgs=orgs,
|
||||
roles=roles,
|
||||
**user_object.dict(),
|
||||
)
|
||||
|
||||
# Insert the user into the database
|
||||
await users.insert_one(user.dict())
|
||||
|
||||
return User(**user.dict())
|
||||
|
||||
|
||||
async def read_user(request: Request, current_user: PublicUser, user_id: str):
|
||||
users = request.app.db["users"]
|
||||
|
||||
# Check if the user exists
|
||||
isUserExists = await users.find_one({"user_id": user_id})
|
||||
|
||||
# Verify rights
|
||||
await verify_user_rights_on_user(request, current_user, "read", user_id)
|
||||
|
||||
# If the user does not exist, raise an error
|
||||
if not isUserExists:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="User does not exist"
|
||||
)
|
||||
|
||||
return User(**isUserExists)
|
||||
|
||||
|
||||
async def update_user(
|
||||
request: Request, user_id: str, user_object: User, current_user: PublicUser
|
||||
):
|
||||
users = request.app.db["users"]
|
||||
|
||||
# Verify rights
|
||||
await verify_user_rights_on_user(request, current_user, "update", user_id)
|
||||
|
||||
isUserExists = await users.find_one({"user_id": user_id})
|
||||
isUsernameAvailable = await users.find_one({"username": user_object.username})
|
||||
isEmailAvailable = await users.find_one({"email": user_object.email})
|
||||
|
||||
if not isUserExists:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="User does not exist"
|
||||
)
|
||||
|
||||
# okay if username is not changed
|
||||
if isUserExists["username"] == user_object.username:
|
||||
user_object.username = user_object.username.lower()
|
||||
|
||||
else:
|
||||
if isUsernameAvailable:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Username already used"
|
||||
)
|
||||
|
||||
if isEmailAvailable:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Email already used"
|
||||
)
|
||||
|
||||
updated_user = {"$set": user_object.dict()}
|
||||
users.update_one({"user_id": user_id}, updated_user)
|
||||
|
||||
return User(**user_object.dict())
|
||||
|
||||
|
||||
async def update_user_password(
|
||||
request: Request,
|
||||
current_user: PublicUser,
|
||||
user_id: str,
|
||||
password_change_form: PasswordChangeForm,
|
||||
):
|
||||
users = request.app.db["users"]
|
||||
|
||||
isUserExists = await users.find_one({"user_id": user_id})
|
||||
|
||||
# Verify rights
|
||||
await verify_user_rights_on_user(request, current_user, "update", user_id)
|
||||
|
||||
if not isUserExists:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="User does not exist"
|
||||
)
|
||||
|
||||
if not await security_verify_password(
|
||||
password_change_form.old_password, isUserExists["password"]
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail="Wrong password"
|
||||
)
|
||||
|
||||
new_password = await security_hash_password(password_change_form.new_password)
|
||||
|
||||
updated_user = {"$set": {"password": new_password}}
|
||||
await users.update_one({"user_id": user_id}, updated_user)
|
||||
|
||||
return {"detail": "Password updated"}
|
||||
|
||||
|
||||
async def delete_user(request: Request, current_user: PublicUser, user_id: str):
|
||||
users = request.app.db["users"]
|
||||
|
||||
isUserExists = await users.find_one({"user_id": user_id})
|
||||
|
||||
# Verify is user has permission to delete the user
|
||||
await verify_user_rights_on_user(request, current_user, "delete", user_id)
|
||||
|
||||
if not isUserExists:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="User does not exist"
|
||||
)
|
||||
|
||||
await users.delete_one({"user_id": user_id})
|
||||
|
||||
return {"detail": "User deleted"}
|
||||
|
||||
|
||||
# Utils & Security functions
|
||||
|
||||
|
||||
async def security_get_user(request: Request, email: str):
|
||||
users = request.app.db["users"]
|
||||
|
||||
user = await users.find_one({"email": email})
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="User with Email does not exist",
|
||||
)
|
||||
|
||||
return UserInDB(**user)
|
||||
|
||||
|
||||
async def get_userid_by_username(request: Request, username: str):
|
||||
users = request.app.db["users"]
|
||||
|
||||
user = await users.find_one({"username": username})
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="User does not exist"
|
||||
)
|
||||
|
||||
return user["user_id"]
|
||||
|
||||
|
||||
async def get_user_by_userid(request: Request, user_id: str):
|
||||
users = request.app.db["users"]
|
||||
|
||||
user = await users.find_one({"user_id": user_id})
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="User does not exist"
|
||||
)
|
||||
|
||||
user = User(**user)
|
||||
return user
|
||||
|
||||
|
||||
async def get_profile_metadata(request: Request, user):
|
||||
users = request.app.db["users"]
|
||||
request.app.db["roles"]
|
||||
|
||||
user = await users.find_one({"user_id": user["user_id"]})
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="User does not exist"
|
||||
)
|
||||
|
||||
return {"user_object": PublicUser(**user), "roles": "random"}
|
||||
|
||||
|
||||
# Verification of the user's permissions on the roles
|
||||
|
||||
|
||||
async def verify_user_rights_on_user(
|
||||
request: Request,
|
||||
current_user: PublicUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
user_id: str,
|
||||
):
|
||||
users = request.app.db["users"]
|
||||
user = UserInDB(**await users.find_one({"user_id": user_id}))
|
||||
|
||||
if action == "create":
|
||||
return True
|
||||
|
||||
if action == "read":
|
||||
await authorization_verify_if_user_is_anon(current_user.user_id)
|
||||
|
||||
if current_user.user_id == user_id:
|
||||
return True
|
||||
|
||||
for org in current_user.orgs:
|
||||
if org.org_id in [org.org_id for org in user.orgs]:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
if action == "update":
|
||||
await authorization_verify_if_user_is_anon(current_user.user_id)
|
||||
|
||||
if current_user.user_id == user_id:
|
||||
return True
|
||||
|
||||
for org in current_user.orgs:
|
||||
if org.org_id in [org.org_id for org in user.orgs]:
|
||||
if org.org_role == "owner":
|
||||
return True
|
||||
|
||||
await authorization_verify_based_on_roles(
|
||||
request, current_user.user_id, "update", user["roles"], user_id
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
if action == "delete":
|
||||
await authorization_verify_if_user_is_anon(current_user.user_id)
|
||||
|
||||
if current_user.user_id == user_id:
|
||||
return True
|
||||
|
||||
for org in current_user.orgs:
|
||||
if org.org_id in [org.org_id for org in user.orgs]:
|
||||
if org.org_role == "owner":
|
||||
return True
|
||||
|
||||
await authorization_verify_based_on_roles(
|
||||
request, current_user.user_id, "update", user["roles"], user_id
|
||||
)
|
||||
70
apps/api/src/services/utils/upload_content.py
Normal file
70
apps/api/src/services/utils/upload_content.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
import os
|
||||
|
||||
from config.config import get_learnhouse_config
|
||||
|
||||
|
||||
async def upload_content(
|
||||
directory: str, org_id: str, file_binary: bytes, file_and_format: str
|
||||
):
|
||||
# Get Learnhouse Config
|
||||
learnhouse_config = get_learnhouse_config()
|
||||
|
||||
# Get content delivery method
|
||||
content_delivery = learnhouse_config.hosting_config.content_delivery.type
|
||||
|
||||
if content_delivery == "filesystem":
|
||||
# create folder for activity
|
||||
if not os.path.exists(f"content/{org_id}/{directory}"):
|
||||
# create folder for activity
|
||||
os.makedirs(f"content/{org_id}/{directory}")
|
||||
# upload file to server
|
||||
with open(
|
||||
f"content/{org_id}/{directory}/{file_and_format}",
|
||||
"wb",
|
||||
) as f:
|
||||
f.write(file_binary)
|
||||
f.close()
|
||||
|
||||
elif content_delivery == "s3api":
|
||||
# Upload to server then to s3 (AWS Keys are stored in environment variables and are loaded by boto3)
|
||||
# TODO: Improve implementation of this
|
||||
print("Uploading to s3...")
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
endpoint_url=learnhouse_config.hosting_config.content_delivery.s3api.endpoint_url,
|
||||
)
|
||||
|
||||
# Create folder for activity
|
||||
if not os.path.exists(f"content/{org_id}/{directory}"):
|
||||
# create folder for activity
|
||||
os.makedirs(f"content/{org_id}/{directory}")
|
||||
|
||||
# Upload file to server
|
||||
with open(
|
||||
f"content/{org_id}/{directory}/{file_and_format}",
|
||||
"wb",
|
||||
) as f:
|
||||
f.write(file_binary)
|
||||
f.close()
|
||||
|
||||
print("Uploading to s3 using boto3...")
|
||||
try:
|
||||
s3.upload_file(
|
||||
f"content/{org_id}/{directory}/{file_and_format}",
|
||||
"learnhouse-media",
|
||||
f"content/{org_id}/{directory}/{file_and_format}",
|
||||
)
|
||||
except ClientError as e:
|
||||
print(e)
|
||||
|
||||
print("Checking if file exists in s3...")
|
||||
try:
|
||||
s3.head_object(
|
||||
Bucket="learnhouse-media",
|
||||
Key=f"content/{org_id}/{directory}/{file_and_format}",
|
||||
)
|
||||
print("File upload successful!")
|
||||
except Exception as e:
|
||||
print(f"An error occurred: {str(e)}")
|
||||
8
apps/web/.eslintrc
Normal file
8
apps/web/.eslintrc
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "next",
|
||||
"rules": {
|
||||
"react/no-unescaped-entities": "off",
|
||||
"@next/next/no-page-custom-font": "off",
|
||||
"@next/next/no-img-element": "off"
|
||||
}
|
||||
}
|
||||
42
apps/web/.gitignore
vendored
Normal file
42
apps/web/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# Sentry
|
||||
.sentryclirc
|
||||
|
||||
# Sentry
|
||||
next.config.original.js
|
||||
17
apps/web/Dockerfile
Normal file
17
apps/web/Dockerfile
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
#
|
||||
FROM node:16-alpine
|
||||
|
||||
#
|
||||
WORKDIR /usr/learnhouse/front
|
||||
|
||||
#
|
||||
COPY package.json /usr/learnhouse/front/package.json
|
||||
|
||||
#
|
||||
RUN npm install
|
||||
|
||||
#
|
||||
COPY ./ /usr/learnhouse
|
||||
|
||||
#
|
||||
CMD ["npm", "run", "dev"]
|
||||
0
apps/web/README.md
Normal file
0
apps/web/README.md
Normal file
19
apps/web/app/api/revalidate/route.ts
Normal file
19
apps/web/app/api/revalidate/route.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { revalidateTag } from "next/cache";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const tag: any = request.nextUrl.searchParams.get("tag");
|
||||
revalidateTag(tag);
|
||||
|
||||
return NextResponse.json(
|
||||
{ revalidated: true, now: Date.now(), tag },
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import PageLoading from "@components/Objects/Loaders/PageLoading";
|
||||
|
||||
export default function Loading() {
|
||||
// Or a custom loading skeleton component
|
||||
return (
|
||||
<PageLoading></PageLoading>
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { default as React, } from "react";
|
||||
import AuthProvider from "@components/Security/AuthProvider";
|
||||
import EditorWrapper from "@components/Objects/Editor/EditorWrapper";
|
||||
import { getCourseMetadataWithAuthHeader } from "@services/courses/courses";
|
||||
import { cookies } from "next/headers";
|
||||
import { Metadata } from "next";
|
||||
import { getActivityWithAuthHeader } from "@services/courses/activities";
|
||||
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth";
|
||||
|
||||
type MetadataProps = {
|
||||
params: { orgslug: string, courseid: string, activityid: string };
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
};
|
||||
|
||||
export async function generateMetadata(
|
||||
{ params }: MetadataProps,
|
||||
): Promise<Metadata> {
|
||||
const cookieStore = cookies();
|
||||
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
||||
// Get Org context information
|
||||
const course_meta = await getCourseMetadataWithAuthHeader(params.courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
|
||||
|
||||
return {
|
||||
title: `Edit - ${course_meta.course.name} Activity`,
|
||||
description: course_meta.course.mini_description,
|
||||
};
|
||||
}
|
||||
|
||||
const EditActivity = async (params: any) => {
|
||||
const cookieStore = cookies();
|
||||
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
||||
const activityid = params.params.activityid;
|
||||
const courseid = params.params.courseid;
|
||||
const orgslug = params.params.orgslug;
|
||||
|
||||
const courseInfo = await getCourseMetadataWithAuthHeader(courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
|
||||
const activity = await getActivityWithAuthHeader(activityid, { revalidate: 0, tags: ['activities'] }, access_token ? access_token : null)
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AuthProvider>
|
||||
<EditorWrapper orgslug={orgslug} course={courseInfo} activity={activity} content={activity.content}></EditorWrapper>
|
||||
</AuthProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditActivity;
|
||||
1
apps/web/app/editor/main.ts
Normal file
1
apps/web/app/editor/main.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const EDITOR = "main";
|
||||
80
apps/web/app/install/install.tsx
Normal file
80
apps/web/app/install/install.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
'use client'
|
||||
import React, { use, useEffect } from 'react'
|
||||
import { INSTALL_STEPS } from './steps/steps'
|
||||
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
|
||||
|
||||
|
||||
|
||||
function InstallClient() {
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
const step: any = parseInt(searchParams.get('step') || '0');
|
||||
const [stepNumber, setStepNumber] = React.useState(step)
|
||||
const [stepsState, setStepsState] = React.useState(INSTALL_STEPS)
|
||||
|
||||
function handleStepChange(stepNumber: number) {
|
||||
setStepNumber(stepNumber)
|
||||
router.push(`/install?step=${stepNumber}`)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setStepNumber(step)
|
||||
}, [step])
|
||||
|
||||
return (
|
||||
<GeneralWrapperStyled>
|
||||
<div className='flex justify-center '>
|
||||
<div className='grow'>
|
||||
<LearnHouseLogo />
|
||||
</div>
|
||||
<div className="steps flex space-x-2 justify-center text-sm p-3 bg-slate-50 rounded-full w-fit m-auto px-10">
|
||||
<div className="flex space-x-8">
|
||||
{stepsState.map((step, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex items-center cursor-pointer space-x-2`}
|
||||
onClick={() => handleStepChange(index)}
|
||||
>
|
||||
<div className={`flex w-7 h-7 rounded-full text-slate-700 bg-slate-200 justify-center items-center m-auto align-middle hover:bg-slate-300 transition-all ${index === stepNumber ? 'bg-slate-300' : ''}`}>
|
||||
{index}
|
||||
</div>
|
||||
<div>{step.name}</div>
|
||||
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex pt-8 flex-col" >
|
||||
<h1 className='font-bold text-3xl'>{stepsState[stepNumber].name}</h1>
|
||||
<div className="pt-8">
|
||||
{stepsState[stepNumber].component}
|
||||
</div>
|
||||
</div>
|
||||
</GeneralWrapperStyled>
|
||||
)
|
||||
}
|
||||
|
||||
const LearnHouseLogo = () => {
|
||||
return (
|
||||
<svg width="133" height="80" viewBox="0 0 433 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="80" height="80" rx="24" fill="black" />
|
||||
<rect width="80" height="80" rx="24" fill="url(#paint0_angular_1555_220)" />
|
||||
<rect x="0.5" y="0.5" width="79" height="79" rx="23.5" stroke="white" strokeOpacity="0.12" />
|
||||
<path d="M37.546 55.926V35.04L33.534 30.497L37.546 29.258V27.016L33.534 22.473L44.626 19.11V55.926L48.992 61H33.18L37.546 55.926Z" fill="white" />
|
||||
<path d="M113.98 54.98V30.2L109.22 24.81L113.98 23.34V20.68L109.22 15.29L122.38 11.3V54.98L127.56 61H108.8L113.98 54.98ZM157.704 41.19V41.26H135.234C136.004 50.29 140.834 54.07 146.294 54.07C151.054 54.07 155.254 51.69 156.304 48.75L157.354 49.17C154.834 55.54 149.864 61.98 141.534 61.98C132.364 61.98 127.184 53.79 127.184 45.39C127.184 36.36 132.784 26 144.194 26C152.524 26 157.634 31.6 157.704 41.05L157.774 41.19H157.704ZM148.674 39.16V38.53C148.674 31.04 145.664 28.1 142.584 28.1C137.264 28.1 135.094 34.47 135.094 38.67V39.16H148.674ZM178.717 61V55.12C176.057 57.71 171.157 61.7 166.537 61.7C161.707 61.7 158.137 59.32 158.137 53.65C158.137 46.51 166.607 42.87 178.717 38.6C178.717 33 178.577 28.66 172.837 28.66C167.237 28.66 163.877 32.58 160.307 37.9H159.817V26.7H188.657L187.117 32.72V56.45H187.187L192.367 61H178.717ZM178.717 53.23V40.56C167.727 44.97 167.377 47.98 167.377 51.34C167.377 54.7 169.687 56.17 172.627 56.17C174.797 56.17 176.967 55.05 178.717 53.23ZM221.429 39.09H220.869C217.789 31.74 213.659 29.29 210.439 29.29C205.609 29.29 205.609 32.79 205.609 39.93V54.98L212.119 61H192.029L197.209 54.98V32.09L192.449 26.7H221.429V39.09ZM261.467 61H242.707L247.747 54.98V39.44C247.747 34.05 246.977 30.62 241.587 30.62C238.997 30.62 236.337 31.74 234.097 34.75V54.98L239.137 61H220.377L225.697 54.98V36.08L220.937 30.69L234.097 26V32.37C236.897 28.03 241.447 25.86 245.647 25.86C252.787 25.86 256.147 30.48 256.147 37.06V54.98L261.467 61ZM274.343 11.3V32.23C277.143 27.89 281.693 25.72 285.893 25.72C293.033 25.72 296.393 30.34 296.393 36.92V54.98H296.463L301.643 61H282.883L287.993 55.05V39.3C287.993 33.91 287.223 30.48 281.833 30.48C279.243 30.48 276.583 31.6 274.343 34.61V54.98L279.523 61H260.763L265.943 54.98V21.38L261.183 15.99L274.343 11.3ZM335.945 42.31C335.945 51.34 329.855 61.84 316.835 61.84C306.895 61.84 301.645 53.79 301.645 45.39C301.645 36.36 307.735 25.86 320.755 25.86C330.695 25.86 335.945 33.91 335.945 42.31ZM316.975 28.52C311.165 28.52 310.535 34.82 310.535 39.02C310.535 49.94 314.525 59.18 320.685 59.18C325.865 59.18 327.195 52.32 327.195 48.68C327.195 37.76 323.135 28.52 316.975 28.52ZM349.01 26.63V48.12C349.01 53.51 349.78 56.94 355.17 56.94C357.55 56.94 360 55.75 361.82 53.65V32.72L356.64 26.63H370.22V55.26L374.98 61L361.82 61.42V55.82C359.3 59.32 356.08 61.7 351.11 61.7C343.97 61.7 340.61 57.08 340.61 50.5V32.72L335.36 26.63H349.01ZM374.617 47.77H375.177C376.997 53.79 382.527 59.04 388.267 59.04C391.137 59.04 393.517 57.64 393.517 54.49C393.517 46.23 374.967 50.29 374.967 36.43C374.967 31.25 379.517 26.7 386.657 26.7H394.357L396.947 25.23V36.85L396.527 36.78C394.007 32.23 389.807 28.87 385.327 28.94C382.387 29.01 380.707 30.83 380.707 33.56C380.707 40.77 399.887 37.62 399.887 50.43C399.887 58.55 391.697 61.7 386.167 61.7C382.667 61.7 377.907 61.21 375.247 60.09L374.617 47.77ZM430.416 41.19V41.26H407.946C408.716 50.29 413.546 54.07 419.006 54.07C423.766 54.07 427.966 51.69 429.016 48.75L430.066 49.17C427.546 55.54 422.576 61.98 414.246 61.98C405.076 61.98 399.896 53.79 399.896 45.39C399.896 36.36 405.496 26 416.906 26C425.236 26 430.346 31.6 430.416 41.05L430.486 41.19H430.416ZM421.386 39.16V38.53C421.386 31.04 418.376 28.1 415.296 28.1C409.976 28.1 407.806 34.47 407.806 38.67V39.16H421.386Z" fill="#121212" />
|
||||
<defs>
|
||||
<radialGradient id="paint0_angular_1555_220" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(40 40) rotate(90) scale(40)">
|
||||
<stop stopColor="#FBFBFB" stopOpacity="0.15" />
|
||||
<stop offset="0.442708" stopOpacity="0.1" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default InstallClient
|
||||
18
apps/web/app/install/page.tsx
Normal file
18
apps/web/app/install/page.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react'
|
||||
import InstallClient from './install'
|
||||
|
||||
|
||||
export const metadata = {
|
||||
title: "Install LearnHouse",
|
||||
description: "Install Learnhouse on your server",
|
||||
}
|
||||
|
||||
function InstallPage() {
|
||||
return (
|
||||
<div className='bg-white h-screen'>
|
||||
<InstallClient />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InstallPage
|
||||
133
apps/web/app/install/steps/account_creation.tsx
Normal file
133
apps/web/app/install/steps/account_creation.tsx
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
"use client";
|
||||
import FormLayout, { ButtonBlack, FormField, FormLabel, FormLabelAndMessage, FormMessage, Input } from '@components/StyledElements/Form/Form'
|
||||
import * as Form from '@radix-ui/react-form';
|
||||
import { getAPIUrl } from '@services/config/config';
|
||||
import { createNewUserInstall, updateInstall } from '@services/install/install';
|
||||
import { swrFetcher } from '@services/utils/ts/requests';
|
||||
import { useFormik } from 'formik';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React from 'react'
|
||||
import { BarLoader } from 'react-spinners';
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
const validate = (values: any) => {
|
||||
const errors: any = {};
|
||||
|
||||
if (!values.email) {
|
||||
errors.email = 'Required';
|
||||
}
|
||||
else if (
|
||||
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
|
||||
) {
|
||||
errors.email = 'Invalid email address';
|
||||
}
|
||||
|
||||
if (!values.password) {
|
||||
errors.password = 'Required';
|
||||
}
|
||||
else if (values.password.length < 8) {
|
||||
errors.password = 'Password must be at least 8 characters';
|
||||
}
|
||||
|
||||
if (!values.confirmPassword) {
|
||||
errors.confirmPassword = 'Required';
|
||||
}
|
||||
else if (values.confirmPassword !== values.password) {
|
||||
errors.confirmPassword = 'Passwords must match';
|
||||
}
|
||||
|
||||
if (!values.username) {
|
||||
errors.username = 'Required';
|
||||
}
|
||||
else if (values.username.length < 3) {
|
||||
errors.username = 'Username must be at least 3 characters';
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
function AccountCreation() {
|
||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||
const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher);
|
||||
const router = useRouter(
|
||||
|
||||
)
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
org_slug: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
username: '',
|
||||
},
|
||||
validate,
|
||||
onSubmit: values => {
|
||||
|
||||
let finalvalues = { ...values, org_slug: install.data[1].slug }
|
||||
let finalvalueswithoutpasswords = { ...values, password: '', confirmPassword: '', org_slug: install.data[1].slug }
|
||||
let install_data = { ...install.data, 3: finalvalues }
|
||||
let install_data_without_passwords = { ...install.data, 3: finalvalueswithoutpasswords }
|
||||
updateInstall({ ...install_data_without_passwords }, 4)
|
||||
createNewUserInstall(finalvalues)
|
||||
|
||||
// await 2 seconds
|
||||
setTimeout(() => {
|
||||
setIsSubmitting(false)
|
||||
}, 2000)
|
||||
|
||||
router.push('/install?step=4')
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormLayout onSubmit={formik.handleSubmit}>
|
||||
<FormField name="email">
|
||||
<FormLabelAndMessage label='Email' message={formik.errors.email} />
|
||||
<Form.Control asChild>
|
||||
<Input onChange={formik.handleChange} value={formik.values.email} type="email" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
{/* for password */}
|
||||
<FormField name="password">
|
||||
<FormLabelAndMessage label='Password' message={formik.errors.password} />
|
||||
|
||||
<Form.Control asChild>
|
||||
<Input onChange={formik.handleChange} value={formik.values.password} type="password" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
{/* for confirm password */}
|
||||
<FormField name="confirmPassword">
|
||||
|
||||
<FormLabelAndMessage label='Confirm Password' message={formik.errors.confirmPassword} />
|
||||
|
||||
<Form.Control asChild>
|
||||
<Input onChange={formik.handleChange} value={formik.values.confirmPassword} type="password" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
{/* for username */}
|
||||
<FormField name="username">
|
||||
|
||||
<FormLabelAndMessage label='Username' message={formik.errors.username} />
|
||||
|
||||
<Form.Control asChild>
|
||||
<Input onChange={formik.handleChange} value={formik.values.username} type="text" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<div className="flex flex-row-reverse py-4">
|
||||
<Form.Submit asChild>
|
||||
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
|
||||
{isSubmitting ? <BarLoader cssOverride={{ borderRadius: 60, }} width={60} color="#ffffff" />
|
||||
: "Create Admin Account"}
|
||||
</ButtonBlack>
|
||||
</Form.Submit>
|
||||
</div>
|
||||
|
||||
</FormLayout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountCreation
|
||||
45
apps/web/app/install/steps/default_elements.tsx
Normal file
45
apps/web/app/install/steps/default_elements.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { getAPIUrl } from '@services/config/config';
|
||||
import { createDefaultElements, updateInstall } from '@services/install/install';
|
||||
import { swrFetcher } from '@services/utils/ts/requests';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React from 'react'
|
||||
import useSWR from "swr";
|
||||
|
||||
function DefaultElements() {
|
||||
const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher);
|
||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||
const [isSubmitted, setIsSubmitted] = React.useState(false);
|
||||
const router = useRouter()
|
||||
|
||||
function createDefElementsAndUpdateInstall() {
|
||||
try {
|
||||
createDefaultElements()
|
||||
// add an {} to the install.data object
|
||||
|
||||
let install_data = { ...install.data, 2: { status: 'OK' } }
|
||||
|
||||
updateInstall(install_data, 3)
|
||||
// await 2 seconds
|
||||
setTimeout(() => {
|
||||
setIsSubmitting(false)
|
||||
}, 2000)
|
||||
|
||||
router.push('/install?step=3')
|
||||
setIsSubmitted(true)
|
||||
}
|
||||
catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex py-10 justify-center items-center space-x-3'>
|
||||
<h1>Install Default Elements </h1>
|
||||
<div onClick={createDefElementsAndUpdateInstall} className='p-3 font-bold bg-gray-200 text-gray-900 rounded-lg hover:cursor-pointer' >
|
||||
Install
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DefaultElements
|
||||
19
apps/web/app/install/steps/disable_install_mode.tsx
Normal file
19
apps/web/app/install/steps/disable_install_mode.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { Check, Link } from 'lucide-react'
|
||||
import React from 'react'
|
||||
|
||||
function DisableInstallMode() {
|
||||
return (
|
||||
<div className='p-4 bg-green-300 text-green-950 rounded-md flex space-x-4 items-center'>
|
||||
<div>
|
||||
<Check size={32} />
|
||||
</div>
|
||||
<div><p className='font-bold text-lg'>You have reached the end of the Installation process, <b><i>please don't forget to disable installation mode.</i></b> </p>
|
||||
<div className='flex space-x-2 items-center'>
|
||||
<Link size={20} />
|
||||
<a rel='noreferrer' target='_blank' className="text-blue-950 font-medium" href="http://docs.learnhouse.app">LearnHouse Docs</a>
|
||||
</div></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DisableInstallMode
|
||||
39
apps/web/app/install/steps/finish.tsx
Normal file
39
apps/web/app/install/steps/finish.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { getAPIUrl } from '@services/config/config';
|
||||
import { updateInstall } from '@services/install/install';
|
||||
import { swrFetcher } from '@services/utils/ts/requests';
|
||||
import { Check } from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React from 'react'
|
||||
import useSWR from "swr";
|
||||
|
||||
const Finish = () => {
|
||||
const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher);
|
||||
const router = useRouter()
|
||||
|
||||
async function finishInstall() {
|
||||
|
||||
let install_data = { ...install.data, 5: { status: 'OK' } }
|
||||
|
||||
let data = await updateInstall(install_data, 6)
|
||||
if (data) {
|
||||
router.push('/install?step=6')
|
||||
}
|
||||
else {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex py-10 justify-center items-center space-x-3'>
|
||||
<h1>Installation Complete</h1>
|
||||
<br />
|
||||
<Check size={32} />
|
||||
<div onClick={finishInstall} className='p-3 font-bold bg-gray-200 text-gray-900 rounded-lg hover:cursor-pointer' >
|
||||
Next Step
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default Finish
|
||||
69
apps/web/app/install/steps/get_started.tsx
Normal file
69
apps/web/app/install/steps/get_started.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import PageLoading from '@components/Objects/Loaders/PageLoading';
|
||||
import { getAPIUrl } from '@services/config/config';
|
||||
import { swrFetcher } from '@services/utils/ts/requests';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React, { use, useEffect } from 'react'
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
function GetStarted() {
|
||||
const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher);
|
||||
const router = useRouter()
|
||||
|
||||
async function startInstallation() {
|
||||
let res = await fetch(`${getAPIUrl()}install/start`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({})
|
||||
})
|
||||
|
||||
if (res.status == 200) {
|
||||
mutate(`${getAPIUrl()}install/latest`)
|
||||
router.refresh();
|
||||
router.push(`/install?step=1`)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
function redirectToStep() {
|
||||
const step = install.step
|
||||
router.push(`/install?step=${step}`)
|
||||
}
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (install) {
|
||||
redirectToStep()
|
||||
}
|
||||
}, [install])
|
||||
|
||||
|
||||
if (error) return <div className='flex py-10 justify-center items-center space-x-3'>
|
||||
<h1>Start a new installation</h1>
|
||||
<div onClick={startInstallation} className='p-3 font-bold bg-green-200 text-green-900 rounded-lg hover:cursor-pointer' >
|
||||
Start
|
||||
</div>
|
||||
</div>
|
||||
|
||||
if (isLoading) return <PageLoading />
|
||||
if (install) {
|
||||
return (
|
||||
<div>
|
||||
<div className='flex py-10 justify-center items-center space-x-3'>
|
||||
<h1>You already started an installation</h1>
|
||||
<div onClick={redirectToStep} className='p-3 font-bold bg-orange-200 text-orange-900 rounded-lg hover:cursor-pointer' >
|
||||
Continue
|
||||
</div>
|
||||
<div onClick={startInstallation} className='p-3 font-bold bg-green-200 text-green-900 rounded-lg hover:cursor-pointer' >
|
||||
Start
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default GetStarted
|
||||
138
apps/web/app/install/steps/org_creation.tsx
Normal file
138
apps/web/app/install/steps/org_creation.tsx
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
|
||||
import FormLayout, { ButtonBlack, FormField, FormLabel, FormLabelAndMessage, FormMessage, Input } from '@components/StyledElements/Form/Form'
|
||||
import * as Form from '@radix-ui/react-form';
|
||||
import { useFormik } from 'formik';
|
||||
import { BarLoader } from 'react-spinners';
|
||||
import React from 'react'
|
||||
import { createNewOrganization } from '@services/organizations/orgs';
|
||||
import { swrFetcher } from '@services/utils/ts/requests';
|
||||
import { getAPIUrl } from '@services/config/config';
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { createNewOrgInstall, updateInstall } from '@services/install/install';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Check } from 'lucide-react';
|
||||
|
||||
const validate = (values: any) => {
|
||||
const errors: any = {};
|
||||
|
||||
if (!values.name) {
|
||||
errors.name = 'Required';
|
||||
}
|
||||
|
||||
if (!values.description) {
|
||||
errors.description = 'Required';
|
||||
}
|
||||
|
||||
if (!values.slug) {
|
||||
errors.slug = 'Required';
|
||||
}
|
||||
|
||||
if (!values.email) {
|
||||
errors.email = 'Required';
|
||||
}
|
||||
else if (
|
||||
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
|
||||
) {
|
||||
errors.email = 'Invalid email address';
|
||||
}
|
||||
|
||||
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
function OrgCreation() {
|
||||
const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher);
|
||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||
const [isSubmitted, setIsSubmitted] = React.useState(false);
|
||||
const router = useRouter()
|
||||
|
||||
|
||||
function createOrgAndUpdateInstall(values: any) {
|
||||
try {
|
||||
createNewOrgInstall(values)
|
||||
install.data = {
|
||||
1: values
|
||||
}
|
||||
let install_data = { ...install.data, 1: values }
|
||||
updateInstall(install_data, 2)
|
||||
// await 2 seconds
|
||||
setTimeout(() => {
|
||||
setIsSubmitting(false)
|
||||
}, 2000)
|
||||
|
||||
router.push('/install?step=2')
|
||||
setIsSubmitted(true)
|
||||
}
|
||||
catch (e) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
name: '',
|
||||
description: '',
|
||||
slug: '',
|
||||
email: '',
|
||||
},
|
||||
validate,
|
||||
onSubmit: values => {
|
||||
createOrgAndUpdateInstall(values)
|
||||
},
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<FormLayout onSubmit={formik.handleSubmit}>
|
||||
<FormField name="name">
|
||||
<FormLabelAndMessage label='Name' message={formik.errors.name} />
|
||||
<Form.Control asChild>
|
||||
<Input onChange={formik.handleChange} value={formik.values.name} type="text" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField name="description">
|
||||
<FormLabelAndMessage label='Description' message={formik.errors.description} />
|
||||
|
||||
<Form.Control asChild>
|
||||
<Input onChange={formik.handleChange} value={formik.values.description} type="text" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField name="slug">
|
||||
|
||||
<FormLabelAndMessage label='Slug' message={formik.errors.slug} />
|
||||
|
||||
<Form.Control asChild>
|
||||
<Input onChange={formik.handleChange} value={formik.values.slug} type="text" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
{/* for username */}
|
||||
<FormField name="email">
|
||||
|
||||
<FormLabelAndMessage label='Email' message={formik.errors.email} />
|
||||
|
||||
<Form.Control asChild>
|
||||
<Input onChange={formik.handleChange} value={formik.values.email} type="email" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<div className="flex flex-row-reverse py-4">
|
||||
<Form.Submit asChild>
|
||||
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
|
||||
{isSubmitting ? <BarLoader cssOverride={{ borderRadius: 60, }} width={60} color="#ffffff" />
|
||||
: "Create Organization"}
|
||||
</ButtonBlack>
|
||||
</Form.Submit>
|
||||
</div>
|
||||
|
||||
{isSubmitted && <div className='flex space-x-3'> <Check /> Organization Created Successfully</div>}
|
||||
|
||||
|
||||
</FormLayout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrgCreation
|
||||
43
apps/web/app/install/steps/sample_data.tsx
Normal file
43
apps/web/app/install/steps/sample_data.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { getAPIUrl } from '@services/config/config';
|
||||
import { createSampleDataInstall, updateInstall } from '@services/install/install';
|
||||
import { swrFetcher } from '@services/utils/ts/requests';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React from 'react'
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
function SampleData() {
|
||||
const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher);
|
||||
const router = useRouter()
|
||||
|
||||
function createSampleData() {
|
||||
|
||||
try {
|
||||
let username = install.data[3].username
|
||||
let slug = install.data[1].slug
|
||||
|
||||
createSampleDataInstall(username, slug)
|
||||
|
||||
let install_data = { ...install.data, 4: { status: 'OK' } }
|
||||
updateInstall(install_data, 5)
|
||||
|
||||
router.push('/install?step=5')
|
||||
|
||||
}
|
||||
catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className='flex py-10 justify-center items-center space-x-3'>
|
||||
<h1>Install Sample data on your organization </h1>
|
||||
<div onClick={createSampleData} className='p-3 font-bold bg-purple-200 text-pruple-900 rounded-lg hover:cursor-pointer' >
|
||||
Start
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SampleData
|
||||
53
apps/web/app/install/steps/steps.tsx
Normal file
53
apps/web/app/install/steps/steps.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import AccountCreation from "./account_creation";
|
||||
import DefaultElements from "./default_elements";
|
||||
import DisableInstallMode from "./disable_install_mode";
|
||||
import Finish from "./finish";
|
||||
import GetStarted from "./get_started";
|
||||
import OrgCreation from "./org_creation";
|
||||
import SampleData from "./sample_data";
|
||||
|
||||
export const INSTALL_STEPS = [
|
||||
{
|
||||
id: "INSTALL_STATUS",
|
||||
name: "Get started",
|
||||
component: <GetStarted />,
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
id: "ORGANIZATION_CREATION",
|
||||
name: "Organization Creation",
|
||||
component: <OrgCreation />,
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
id: "DEFAULT_ELEMENTS",
|
||||
name: "Default Elements",
|
||||
component: <DefaultElements />,
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
id: "ACCOUNT_CREATION",
|
||||
name: "Account Creation",
|
||||
component: <AccountCreation />,
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
id: "SAMPLE_DATA",
|
||||
name: "Sample Data",
|
||||
component: <SampleData />,
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
id: "FINISH",
|
||||
name: "Finish",
|
||||
component: <Finish />,
|
||||
completed: false,
|
||||
|
||||
},
|
||||
{
|
||||
id: "DISABLING_INSTALLATION_MODE",
|
||||
name: "Disabling Installation Mode",
|
||||
component: <DisableInstallMode />,
|
||||
completed: false,
|
||||
},
|
||||
];
|
||||
31
apps/web/app/layout.tsx
Normal file
31
apps/web/app/layout.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
"use client";
|
||||
import "../styles/globals.css";
|
||||
import StyledComponentsRegistry from "../components/Utils/libs/styled-registry";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const variants = {
|
||||
hidden: { opacity: 0, x: 0, y: 0 },
|
||||
enter: { opacity: 1, x: 0, y: 0 },
|
||||
exit: { opacity: 0, x: 0, y: 0 },
|
||||
};
|
||||
return (
|
||||
<html className="" lang="en">
|
||||
<head />
|
||||
<body>
|
||||
<StyledComponentsRegistry>
|
||||
<motion.main
|
||||
variants={variants} // Pass the variant object into Framer Motion
|
||||
initial="hidden" // Set the initial state to variants.hidden
|
||||
animate="enter" // Animated state to variants.enter
|
||||
exit="exit" // Exit state (used later) to variants.exit
|
||||
transition={{ type: "linear" }} // Set the transition to linear
|
||||
className=""
|
||||
>
|
||||
{children}
|
||||
</motion.main>
|
||||
</StyledComponentsRegistry>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
51
apps/web/app/organizations/new/page.tsx
Normal file
51
apps/web/app/organizations/new/page.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
"use client";
|
||||
import React from "react";
|
||||
import { createNewOrganization } from "../../../services/organizations/orgs";
|
||||
|
||||
const Organizations = () => {
|
||||
const [name, setName] = React.useState("");
|
||||
const [description, setDescription] = React.useState("");
|
||||
const [email, setEmail] = React.useState("");
|
||||
const [slug, setSlug] = React.useState("");
|
||||
|
||||
const handleNameChange = (e: any) => {
|
||||
setName(e.target.value);
|
||||
};
|
||||
|
||||
const handleDescriptionChange = (e: any) => {
|
||||
setDescription(e.target.value);
|
||||
};
|
||||
|
||||
const handleEmailChange = (e: any) => {
|
||||
setEmail(e.target.value);
|
||||
};
|
||||
|
||||
const handleSlugChange = (e: any) => {
|
||||
setSlug(e.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
|
||||
let logo = ''
|
||||
const status = await createNewOrganization({ name, description, email, logo, slug, default: false });
|
||||
alert(JSON.stringify(status));
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="font-bold text-lg">New Organization</div>
|
||||
Name: <input onChange={handleNameChange} type="text" />
|
||||
<br />
|
||||
Description: <input onChange={handleDescriptionChange} type="text" />
|
||||
<br />
|
||||
Slug: <input onChange={handleSlugChange} type="text" />
|
||||
<br />
|
||||
Email Address: <input onChange={handleEmailChange} type="text" />
|
||||
<br />
|
||||
<button onClick={handleSubmit}>Create</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Organizations;
|
||||
56
apps/web/app/organizations/page.tsx
Normal file
56
apps/web/app/organizations/page.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
"use client"; //todo: use server components
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
import { deleteOrganizationFromBackend } from "@services/organizations/orgs";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { swrFetcher } from "@services/utils/ts/requests";
|
||||
import { getAPIUrl, getUriWithOrg } from "@services/config/config";
|
||||
import AuthProvider from "@components/Security/AuthProvider";
|
||||
|
||||
const Organizations = () => {
|
||||
const { data: organizations, error } = useSWR(`${getAPIUrl()}orgs/user/page/1/limit/10`, swrFetcher)
|
||||
|
||||
async function deleteOrganization(org_id: any) {
|
||||
const response = await deleteOrganizationFromBackend(org_id);
|
||||
response && mutate(`${getAPIUrl()}orgs/user/page/1/limit/10`, organizations.filter((org: any) => org.org_id !== org_id));
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AuthProvider />
|
||||
<div className="font-bold text-lg">
|
||||
Your Organizations{" "}
|
||||
<Link href="/organizations/new">
|
||||
<button className="bg-blue-500 text-white px-2 py-1 rounded-md hover:bg-blue-600 focus:outline-none">
|
||||
+
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
{error && <p className="text-red-500">Failed to load</p>}
|
||||
{!organizations ? (
|
||||
<p className="text-gray-500">Loading...</p>
|
||||
) : (
|
||||
<div>
|
||||
{organizations.map((org: any) => (
|
||||
<div key={org.org_id} className="flex items-center justify-between mb-4">
|
||||
<Link href={getUriWithOrg(org.slug, "/")}>
|
||||
<h3 className="text-blue-500 cursor-pointer hover:underline">{org.name}</h3>
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => deleteOrganization(org.org_id)}
|
||||
className="px-3 py-1 text-white bg-red-500 rounded-md hover:bg-red-600 focus:outline-none"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Organizations;
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
'use client'; // Error components must be Client Components
|
||||
|
||||
import ErrorUI from '@components/StyledElements/Error/Error';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error;
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
// Log the error to an error reporting service
|
||||
console.error(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ErrorUI></ErrorUI>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue