From dee9ae6cf3e5d782c65c2ec5d8eb89efc8c75e06 Mon Sep 17 00:00:00 2001 From: swve Date: Fri, 24 Mar 2023 22:50:08 +0100 Subject: [PATCH 1/9] feat: use yaml and envs to load config --- app.py | 22 ++++++-- config/config.py | 111 ++++++++++++++++++++++++++++++++++++++ config/config.yaml | 21 ++++++++ config/config.yml | 0 front/tsconfig.json | 2 +- requirements.txt | 3 +- src/core/events/events.py | 4 ++ src/core/events/logs.py | 24 +++++++++ 8 files changed, 181 insertions(+), 6 deletions(-) create mode 100644 config/config.yaml delete mode 100644 config/config.yml create mode 100644 src/core/events/logs.py diff --git a/app.py b/app.py index c4f356a2..f62b2bba 100644 --- a/app.py +++ b/app.py @@ -1,6 +1,8 @@ +import asyncio import logging from fastapi import FastAPI, Request import re +from config.config import LearnHouseConfig, get_learnhouse_config from src.core.config.config import Settings, get_settings from src.core.events.events import shutdown_app, startup_app from src.main import global_router @@ -16,10 +18,13 @@ from fastapi_jwt_auth.exceptions import AuthJWTException # (c) LearnHouse 2022 ######################## +# Get LearnHouse Config +learnhouse_config: LearnHouseConfig = get_learnhouse_config() + # Global Config app = FastAPI( - title="LearnHouse", - description="LearnHouse is a new open-source platform tailored for learning experiences.", + title=learnhouse_config.site_name, + description=learnhouse_config.site_description, version="0.1.0", root_path="/" ) @@ -45,7 +50,7 @@ app.add_event_handler("shutdown", shutdown_app(app)) # JWT Exception Handler -@app.exception_handler(AuthJWTException) +@ app.exception_handler(AuthJWTException) def authjwt_exception_handler(request: Request, exc: AuthJWTException): return JSONResponse( status_code=exc.status_code, # type: ignore @@ -59,10 +64,19 @@ app.include_router(global_router) # General Routes -@app.get("/") +@ app.get("/") async def root(): return {"Message": "Welcome to LearnHouse ✨"} +# Get config + + +@ app.get("/config") +async def config(): + logging.info("Getting config") + config = get_learnhouse_config() + return config.dict() + # @app.get("/initial_data") # async def initial_data(request: Request): diff --git a/config/config.py b/config/config.py index e69de29b..e94ae341 100644 --- a/config/config.py +++ b/config/config.py @@ -0,0 +1,111 @@ +from pydantic import BaseModel +import os +import yaml + + +class HostingConfig(BaseModel): + domain: str + port: int + ssl: bool + use_default_org: bool + allowed_origins: list + allowed_regexp: str + self_hosted: bool + + +class DatabaseConfig(BaseModel): + host: str + port: int + user: str + password: str + database_name: str + + +class LearnHouseConfig(BaseModel): + site_name: str + site_description: str + contact_email: str + hosting_config: HostingConfig + database_config: DatabaseConfig + + +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) + + # 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') + env_port = 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_allowed_regexp = os.environ.get('LEARNHOUSE_ALLOWED_REGEXP') + env_self_hosted = os.environ.get('LEARNHOUSE_SELF_HOSTED') + env_host = os.environ.get('LEARNHOUSE_DB_HOST') + env_db_port = os.environ.get('LEARNHOUSE_DB_PORT') + env_user = os.environ.get('LEARNHOUSE_DB_USER') + env_password = os.environ.get('LEARNHOUSE_DB_PASSWORD') + env_database_name = os.environ.get('LEARNHOUSE_DB_NAME') + + # 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') + port = env_port or yaml_config.get('hosting_config', {}).get('port') + 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') + + host = env_host or yaml_config.get('database_config', {}).get('host') + db_port = env_db_port or yaml_config.get('database_config', {}).get('port') + user = env_user or yaml_config.get('database_config', {}).get('user') + password = env_password or yaml_config.get( + 'database_config', {}).get('password') + database_name = env_database_name or yaml_config.get( + 'database_config', {}).get('database_name') + + # Create HostingConfig and DatabaseConfig objects + hosting_config = HostingConfig( + domain=domain, + port=int(port), + ssl=bool(ssl), + use_default_org=bool(use_default_org), + allowed_origins=list(allowed_origins), + allowed_regexp=allowed_regexp, + self_hosted=bool(self_hosted) + ) + database_config = DatabaseConfig( + host=host, + port=int(db_port), + user=user, + password=password, + database_name=database_name + ) + + # Create LearnHouseConfig object + config = LearnHouseConfig( + site_name=site_name, + site_description=site_description, + contact_email=contact_email, + hosting_config=hosting_config, + database_config=database_config + ) + + return config diff --git a/config/config.yaml b/config/config.yaml new file mode 100644 index 00000000..242fb31c --- /dev/null +++ b/config/config.yaml @@ -0,0 +1,21 @@ +site_name: LearnHouse +site_description: LearnHouse is an open-source platform tailored for learning experiences. +contact_email: hi@learnhouse.app + +hosting_config: + domain: learnhouse.app + port: 443 + ssl: true + use_default_org: false + allowed_origins: + - https://learnhouse.app + - https://learnhouse.io + allowed_regexp: "^https://(.*\\.)?learnhouse\\.app$" + self_hosted: false + +database_config: + host: db.mongo + port: 5432 + user: myuser + password: mypassword + database_name: mydatabase diff --git a/config/config.yml b/config/config.yml deleted file mode 100644 index e69de29b..00000000 diff --git a/front/tsconfig.json b/front/tsconfig.json index 31ffef7d..e8fc70c6 100644 --- a/front/tsconfig.json +++ b/front/tsconfig.json @@ -29,7 +29,7 @@ "@editor/*": ["components/Editor/*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx","**/**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } diff --git a/requirements.txt b/requirements.txt index d1f1bd3a..b6abfd01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,5 @@ python-jose passlib fastapi-jwt-auth faker -requests \ No newline at end of file +requests +pyyaml \ No newline at end of file diff --git a/src/core/events/events.py b/src/core/events/events.py index daa8cc01..27dd830b 100644 --- a/src/core/events/events.py +++ b/src/core/events/events.py @@ -1,6 +1,7 @@ from typing import Callable from fastapi import FastAPI from src.core.events.database import close_database, connect_to_db +from src.core.events.logs import create_logs_dir def startup_app(app: FastAPI) -> Callable: @@ -8,6 +9,9 @@ def startup_app(app: FastAPI) -> Callable: # Connect to database await connect_to_db(app) + # Create logs directory + await create_logs_dir() + return start_app diff --git a/src/core/events/logs.py b/src/core/events/logs.py new file mode 100644 index 00000000..08550073 --- /dev/null +++ b/src/core/events/logs.py @@ -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") From ae280b2cfba61c9973bd3c2802de4a1baf3bccac Mon Sep 17 00:00:00 2001 From: swve Date: Fri, 24 Mar 2023 23:01:44 +0100 Subject: [PATCH 2/9] feat: change organization schema --- src/services/orgs.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/services/orgs.py b/src/services/orgs.py index d5576c98..90b13586 100644 --- a/src/services/orgs.py +++ b/src/services/orgs.py @@ -16,12 +16,11 @@ class Organization(BaseModel): description: str email: str slug: str + default: bool class OrganizationInDB(Organization): org_id: str - owners: List[str] - admins: List[str] class PublicOrganization(Organization): @@ -75,15 +74,13 @@ async def create_org(request: Request, org_object: Organization, current_user: P # generate org_id with uuid4 org_id = str(f"org_{uuid4()}") - org = OrganizationInDB(org_id=org_id, owners=[ - current_user.user_id], admins=[ - current_user.user_id], **org_object.dict()) + 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()}}) @@ -113,7 +110,7 @@ async def update_org(request: Request, org_object: Organization, org_id: str, cu status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist") updated_org = OrganizationInDB( - org_id=org_id, owners=owners, admins=admins, **org_object.dict()) + org_id=org_id, **org_object.dict()) await orgs.update_one({"org_id": org_id}, {"$set": updated_org.dict()}) @@ -149,10 +146,10 @@ async def get_orgs_by_user(request: Request, user_id: str, page: int = 1, limit: orgs = request.app.db["organizations"] user = request.app.db["users"] - # get user orgs + # get user orgs user_orgs = await user.find_one({"user_id": user_id}) - org_ids : list[UserOrganization] = [] + 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": From aa90283fe77eb4680cead357eb63071c518ade32 Mon Sep 17 00:00:00 2001 From: swve Date: Fri, 24 Mar 2023 23:58:09 +0100 Subject: [PATCH 3/9] feat: improve middleware implementation --- front/middleware.ts | 38 ++++++++++++--------------------- front/services/config/config.ts | 27 ++++++++++++++++++----- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/front/middleware.ts b/front/middleware.ts index 8093b6ca..15a48cbc 100644 --- a/front/middleware.ts +++ b/front/middleware.ts @@ -1,3 +1,4 @@ +import { getDefaultOrg, getSelfHostedOption } from "@services/config/config"; import { NextRequest, NextResponse } from "next/server"; export const config = { @@ -16,41 +17,30 @@ export const config = { export default function middleware(req: NextRequest) { const url = req.nextUrl; - - // Get hostname of request (e.g. demo.vercel.pub, demo.localhost:3000) + const isSelfHosted = getSelfHostedOption(); const hostname = req.headers.get("host") || "learnhouse.app"; + let currentHost = hostname.replace(".localhost:3000", ""); + + if (!isSelfHosted && currentHost === ("localhost:3000")) { + // Redirect to error page if not self-hosted and on localhost + const errorUrl = "/error"; + return NextResponse.redirect(errorUrl, { status: 302 }); + } - /* You have to replace ".vercel.pub" with your own domain if you deploy this example under your domain. - You can also use wildcard subdomains on .vercel.app links that are associated with your Vercel team slug - in this case, our team slug is "platformize", thus *.platformize.vercel.app works. Do note that you'll - still need to add "*.platformize.vercel.app" as a wildcard domain on your Vercel dashboard. */ - let currentHost = - process.env.NODE_ENV === "production" && process.env.VERCEL === "1" - ? hostname.replace(`.vercel.pub`, "").replace(`.platformize.vercel.app`, "") - : hostname.replace(`.localhost:3000`, ""); - - /* Editor route */ if (url.pathname.match(/^\/course\/[^/]+\/activity\/[^/]+\/edit$/)) { url.pathname = `/_editor${url.pathname}`; - console.log("editor route", url.pathname); - return NextResponse.rewrite(url, { headers: { orgslug: currentHost } }); } - /* Organizations route */ if (url.pathname.startsWith("/organizations")) { - url.pathname = url.pathname.replace("/organizations", `/organizations${currentHost}`); - // remove localhost:3000 from url - url.pathname = url.pathname.replace(`localhost:3000`, ""); - - + url.pathname = url.pathname.replace("/organizations", `/organizations${currentHost}`).replace("localhost:3000", ""); return NextResponse.rewrite(url); } - console.log("currentHost", url); + if (isSelfHosted) { + currentHost = getDefaultOrg() || currentHost; + } - // rewrite everything else to `/_sites/[site] dynamic route url.pathname = `/_orgs/${currentHost}${url.pathname}`; - - return NextResponse.rewrite(url, { headers: { olgslug: currentHost } }); + return NextResponse.rewrite(url, { headers: { orgslug: currentHost } }); } diff --git a/front/services/config/config.ts b/front/services/config/config.ts index caec07fe..08ee743a 100644 --- a/front/services/config/config.ts +++ b/front/services/config/config.ts @@ -5,14 +5,31 @@ export const getAPIUrl = () => LEARNHOUSE_API_URL; export const getBackendUrl = () => LEARNHOUSE_BACKEND_URL; +export const getSelfHostedOption = () => false; + export const getUriWithOrg = (orgslug: string, path: string) => { + const selfHosted = getSelfHostedOption(); + if (selfHosted) { + return `http://localhost:3000${path}`; + } return `http://${orgslug}.localhost:3000${path}`; }; export const getOrgFromUri = () => { - const hostname = window.location.hostname; - // get the orgslug from the hostname - const orgslug = hostname.split(".")[0]; - return orgslug; - + const selfHosted = getSelfHostedOption(); + if (selfHosted) { + getDefaultOrg(); + } else { + const hostname = window.location.hostname; + // get the orgslug from the hostname + const orgslug = hostname.split(".")[0]; + return orgslug; + } +}; + +export const getDefaultOrg = () => { + const selfHosted = getSelfHostedOption(); + if (selfHosted) { + return "test"; + } }; From a0e749399fc25a627af02537aa32a630de197f42 Mon Sep 17 00:00:00 2001 From: swve Date: Fri, 24 Mar 2023 23:59:22 +0100 Subject: [PATCH 4/9] chore: delete depreciated config folder --- src/core/config/__init__.py | 0 src/core/config/config.py | 11 ----------- 2 files changed, 11 deletions(-) delete mode 100644 src/core/config/__init__.py delete mode 100644 src/core/config/config.py diff --git a/src/core/config/__init__.py b/src/core/config/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/core/config/config.py b/src/core/config/config.py deleted file mode 100644 index 99b429d5..00000000 --- a/src/core/config/config.py +++ /dev/null @@ -1,11 +0,0 @@ -from fastapi import FastAPI - -class Settings(FastAPI): - title="LearnHousse", - description="LearnHouse is a new open-source platform tailored for learning experiences.", - version="0.1.0", - root_path="/" - docs_url="/docs" - -async def get_settings() -> Settings: - return Settings() \ No newline at end of file From 22464cead462f080cca0631a7f596b844c26827a Mon Sep 17 00:00:00 2001 From: swve Date: Sat, 25 Mar 2023 00:24:20 +0100 Subject: [PATCH 5/9] refactor: move security services to security dir --- src/routers/activity.py | 2 +- src/routers/auth.py | 2 +- src/routers/blocks.py | 2 +- src/routers/courses/activities.py | 2 +- src/routers/courses/chapters.py | 2 +- src/routers/courses/collections.py | 2 +- src/routers/courses/courses.py | 2 +- src/routers/houses.py | 2 +- src/routers/orgs.py | 2 +- src/routers/roles.py | 2 +- src/routers/users.py | 2 +- src/{dependencies => security}/__init__.py | 0 src/{dependencies => security}/auth.py | 2 +- src/{services => security}/security.py | 0 src/services/courses/activities/activities.py | 2 +- src/services/courses/activities/video.py | 2 +- src/services/courses/chapters.py | 2 +- src/services/courses/collections.py | 2 +- src/services/courses/courses.py | 2 +- src/services/houses.py | 2 +- src/services/orgs.py | 2 +- src/services/roles/roles.py | 2 +- src/services/users/users.py | 2 +- 23 files changed, 21 insertions(+), 21 deletions(-) rename src/{dependencies => security}/__init__.py (100%) rename src/{dependencies => security}/auth.py (98%) rename src/{services => security}/security.py (100%) diff --git a/src/routers/activity.py b/src/routers/activity.py index 91479bef..6ea6e04c 100644 --- a/src/routers/activity.py +++ b/src/routers/activity.py @@ -1,5 +1,5 @@ from fastapi import APIRouter, Depends, Request -from src.dependencies.auth import get_current_user +from src.security.auth import get_current_user from src.services.activity import Activity, add_activity_to_activity, close_activity, create_activity, get_user_activities, get_user_activities_orgslug diff --git a/src/routers/auth.py b/src/routers/auth.py index 9c8bf905..27dd78cf 100644 --- a/src/routers/auth.py +++ b/src/routers/auth.py @@ -1,7 +1,7 @@ from urllib.request import Request from fastapi import Depends, APIRouter, HTTPException, status, Request from fastapi.security import OAuth2PasswordRequestForm -from src.dependencies.auth import * +from src.security.auth import * from src.services.users.users import * from datetime import timedelta from fastapi.responses import JSONResponse diff --git a/src/routers/blocks.py b/src/routers/blocks.py index 6cf26d63..6d90bb32 100644 --- a/src/routers/blocks.py +++ b/src/routers/blocks.py @@ -1,5 +1,5 @@ from fastapi import APIRouter, Depends, UploadFile, Form, Request -from src.dependencies.auth import get_current_user +from src.security.auth import get_current_user from fastapi import HTTPException, status, UploadFile 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 diff --git a/src/routers/courses/activities.py b/src/routers/courses/activities.py index f77e1225..c0cc5f8b 100644 --- a/src/routers/courses/activities.py +++ b/src/routers/courses/activities.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, Depends, UploadFile, Form, Request from src.services.courses.activities.activities import * -from src.dependencies.auth import get_current_user +from src.security.auth import get_current_user from src.services.courses.activities.video import create_video_activity router = APIRouter() diff --git a/src/routers/courses/chapters.py b/src/routers/courses/chapters.py index 57d42fec..aa024cae 100644 --- a/src/routers/courses/chapters.py +++ b/src/routers/courses/chapters.py @@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, Request, UploadFile, Form from src.services.courses.chapters import CourseChapter, CourseChapterMetaData, create_coursechapter, delete_coursechapter, get_coursechapter, get_coursechapters, get_coursechapters_meta, update_coursechapter, update_coursechapters_meta from src.services.users.users import PublicUser -from src.dependencies.auth import get_current_user +from src.security.auth import get_current_user router = APIRouter() diff --git a/src/routers/courses/collections.py b/src/routers/courses/collections.py index 26293524..fe5f8f17 100644 --- a/src/routers/courses/collections.py +++ b/src/routers/courses/collections.py @@ -1,5 +1,5 @@ from fastapi import APIRouter, Depends, Request -from src.dependencies.auth import get_current_user +from src.security.auth import get_current_user from src.services.users.users import PublicUser, User from src.services.courses.collections import Collection, create_collection, get_collection, get_collections, update_collection, delete_collection diff --git a/src/routers/courses/courses.py b/src/routers/courses/courses.py index f440d8e5..6bd0d20e 100644 --- a/src/routers/courses/courses.py +++ b/src/routers/courses/courses.py @@ -1,5 +1,5 @@ from fastapi import APIRouter, Depends, UploadFile, Form, Request -from src.dependencies.auth import get_current_user +from src.security.auth import get_current_user from src.services.courses.courses import Course, create_course, get_course, get_course_meta, get_courses, get_courses_orgslug, update_course, delete_course, update_course_thumbnail from src.services.users.users import PublicUser diff --git a/src/routers/houses.py b/src/routers/houses.py index 8827402f..16782197 100644 --- a/src/routers/houses.py +++ b/src/routers/houses.py @@ -1,5 +1,5 @@ from fastapi import APIRouter, Depends, Request -from src.dependencies.auth import get_current_user +from src.security.auth import get_current_user from src.services.houses import House, HouseInDB, create_house, get_house, get_houses, update_house, delete_house from src.services.users.users import PublicUser, User diff --git a/src/routers/orgs.py b/src/routers/orgs.py index e571195e..579c0c02 100644 --- a/src/routers/orgs.py +++ b/src/routers/orgs.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, Depends, Request -from src.dependencies.auth import get_current_user +from src.security.auth import get_current_user from src.services.orgs import Organization, create_org, delete_org, get_organization, get_organization_by_slug, get_orgs_by_user, update_org from src.services.users.users import PublicUser, User diff --git a/src/routers/roles.py b/src/routers/roles.py index 903125e3..e784324d 100644 --- a/src/routers/roles.py +++ b/src/routers/roles.py @@ -1,5 +1,5 @@ from fastapi import APIRouter, Depends, Request -from src.dependencies.auth import get_current_user +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, User diff --git a/src/routers/users.py b/src/routers/users.py index 8d79b7d0..3a17567c 100644 --- a/src/routers/users.py +++ b/src/routers/users.py @@ -1,7 +1,7 @@ from fastapi import Depends, FastAPI, APIRouter from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from pydantic import BaseModel -from src.dependencies.auth import * +from src.security.auth import * from src.services.users.schemas.users import PasswordChangeForm, PublicUser, User, UserWithPassword from src.services.users.users import create_user, delete_user, get_profile_metadata, get_user_by_userid, read_user, update_user, update_user_password diff --git a/src/dependencies/__init__.py b/src/security/__init__.py similarity index 100% rename from src/dependencies/__init__.py rename to src/security/__init__.py diff --git a/src/dependencies/auth.py b/src/security/auth.py similarity index 98% rename from src/dependencies/auth.py rename to src/security/auth.py index 1e3f0c02..60a1487a 100644 --- a/src/dependencies/auth.py +++ b/src/security/auth.py @@ -6,7 +6,7 @@ from jose import JWTError, jwt from datetime import datetime, timedelta from src.services.users.users import * from fastapi import Cookie, FastAPI -from src.services.security import * +from src.security.security import * from fastapi_jwt_auth import AuthJWT from fastapi_jwt_auth.exceptions import AuthJWTException diff --git a/src/services/security.py b/src/security/security.py similarity index 100% rename from src/services/security.py rename to src/security/security.py diff --git a/src/services/courses/activities/activities.py b/src/services/courses/activities/activities.py index b3205925..1adf49b6 100644 --- a/src/services/courses/activities/activities.py +++ b/src/services/courses/activities/activities.py @@ -1,5 +1,5 @@ from pydantic import BaseModel -from src.services.security import verify_user_rights_with_roles +from src.security.security import verify_user_rights_with_roles from src.services.users.schemas.users import PublicUser, User from fastapi import FastAPI, HTTPException, status, Request, Response, BackgroundTasks, UploadFile, File from uuid import uuid4 diff --git a/src/services/courses/activities/video.py b/src/services/courses/activities/video.py index d921585f..f4ac26aa 100644 --- a/src/services/courses/activities/video.py +++ b/src/services/courses/activities/video.py @@ -1,5 +1,5 @@ from pydantic import BaseModel -from src.services.security import verify_user_rights_with_roles +from src.security.security import verify_user_rights_with_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 diff --git a/src/services/courses/chapters.py b/src/services/courses/chapters.py index 460a0d97..a2a4e1c1 100644 --- a/src/services/courses/chapters.py +++ b/src/services/courses/chapters.py @@ -5,7 +5,7 @@ from uuid import uuid4 from pydantic import BaseModel from src.services.courses.courses import Course, CourseInDB from src.services.courses.activities.activities import Activity, ActivityInDB -from src.services.security import verify_user_rights_with_roles +from src.security.security import verify_user_rights_with_roles from src.services.users.users import PublicUser from fastapi import HTTPException, status, Request, Response, BackgroundTasks, UploadFile, File diff --git a/src/services/courses/collections.py b/src/services/courses/collections.py index 63eca6f7..387d910d 100644 --- a/src/services/courses/collections.py +++ b/src/services/courses/collections.py @@ -3,7 +3,7 @@ from typing import List from uuid import uuid4 from pydantic import BaseModel from src.services.users.users import PublicUser, User -from src.services.security import * +from src.security.security import * from fastapi import FastAPI, HTTPException, status, Request, Response, BackgroundTasks from datetime import datetime diff --git a/src/services/courses/courses.py b/src/services/courses/courses.py index a2560cd4..5e568904 100644 --- a/src/services/courses/courses.py +++ b/src/services/courses/courses.py @@ -5,7 +5,7 @@ from pydantic import BaseModel from src.services.courses.activities.activities import ActivityInDB from src.services.courses.thumbnails import upload_thumbnail from src.services.users.users import PublicUser -from src.services.security import * +from src.security.security import * from fastapi import HTTPException, status, UploadFile from datetime import datetime diff --git a/src/services/houses.py b/src/services/houses.py index 5023302b..ecca7b68 100644 --- a/src/services/houses.py +++ b/src/services/houses.py @@ -3,7 +3,7 @@ from typing import List from uuid import uuid4 from pydantic import BaseModel from src.services.users.users import PublicUser, User -from src.services.security import * +from src.security.security import * from fastapi import FastAPI, HTTPException, status, Request, Response, BackgroundTasks from datetime import datetime diff --git a/src/services/orgs.py b/src/services/orgs.py index 90b13586..28fd642a 100644 --- a/src/services/orgs.py +++ b/src/services/orgs.py @@ -4,7 +4,7 @@ from uuid import uuid4 from pydantic import BaseModel from src.services.users.schemas.users import UserOrganization from src.services.users.users import PublicUser, User -from src.services.security import * +from src.security.security import * from fastapi import FastAPI, HTTPException, status, Request, Response, BackgroundTasks from datetime import datetime diff --git a/src/services/roles/roles.py b/src/services/roles/roles.py index cae2585c..67f34594 100644 --- a/src/services/roles/roles.py +++ b/src/services/roles/roles.py @@ -4,7 +4,7 @@ from uuid import uuid4 from pydantic import BaseModel from src.services.roles.schemas.roles import Role, RoleInDB from src.services.users.schemas.users import PublicUser, User -from src.services.security import * +from src.security.security import * from src.services.houses import House from fastapi import HTTPException, status, Request from datetime import datetime diff --git a/src/services/users/users.py b/src/services/users/users.py index 7a4fdc6d..e64adcc4 100644 --- a/src/services/users/users.py +++ b/src/services/users/users.py @@ -3,7 +3,7 @@ from typing import Literal from uuid import uuid4 from fastapi import HTTPException, Request, status from src.services.roles.schemas.roles import Role -from src.services.security import security_hash_password, security_verify_password +from src.security.security import security_hash_password, security_verify_password from src.services.users.schemas.users import PasswordChangeForm, PublicUser, User, UserOrganization, UserWithPassword, UserInDB From 51aecbb6e31d31a3eba38032e93935ed541a5e0d Mon Sep 17 00:00:00 2001 From: swve Date: Sat, 25 Mar 2023 00:25:17 +0100 Subject: [PATCH 6/9] feat: depreciate houses service & route --- src/main.py | 3 +- src/routers/houses.py | 49 ------------- src/services/houses.py | 157 ----------------------------------------- 3 files changed, 1 insertion(+), 208 deletions(-) delete mode 100644 src/routers/houses.py delete mode 100644 src/services/houses.py diff --git a/src/main.py b/src/main.py index 3e9f6942..f85d6cbb 100644 --- a/src/main.py +++ b/src/main.py @@ -1,5 +1,5 @@ from fastapi import APIRouter -from src.routers import activity, blocks, users, auth, houses, orgs, roles +from src.routers import activity, blocks, users, auth, orgs, roles from src.routers.courses import chapters, collections, courses,activities @@ -9,7 +9,6 @@ global_router = APIRouter(prefix="/api") # API Routes global_router.include_router(users.router, prefix="/users", tags=["users"]) global_router.include_router(auth.router, prefix="/auth", tags=["auth"]) -global_router.include_router(houses.router, prefix="/houses", tags=["houses"]) global_router.include_router(orgs.router, prefix="/orgs", tags=["orgs"]) global_router.include_router(roles.router, prefix="/roles", tags=["roles"]) global_router.include_router(blocks.router, prefix="/blocks", tags=["blocks"]) diff --git a/src/routers/houses.py b/src/routers/houses.py deleted file mode 100644 index 16782197..00000000 --- a/src/routers/houses.py +++ /dev/null @@ -1,49 +0,0 @@ -from fastapi import APIRouter, Depends, Request -from src.security.auth import get_current_user - -from src.services.houses import House, HouseInDB, create_house, get_house, get_houses, update_house, delete_house -from src.services.users.users import PublicUser, User - - -router = APIRouter() - - -@router.post("/") -async def api_create_house(request: Request,house_object: House, current_user: PublicUser = Depends(get_current_user)): - """ - Create new house - """ - return await create_house(request, house_object, current_user) - - -@router.get("/{house_id}") -async def api_get_house(request: Request,house_id: str, current_user: PublicUser = Depends(get_current_user)): - """ - Get single House by house_id - """ - return await get_house(request, house_id, current_user=current_user) - - -@router.get("/page/{page}/limit/{limit}") -async def api_get_house_by(request: Request,page: int, limit: int): - """ - Get houses by page and limit - """ - return await get_houses(request, page, limit) - - -@router.put("/{house_id}") -async def api_update_house(request: Request,house_object: House, house_id: str, current_user: PublicUser = Depends(get_current_user)): - """ - Update House by house_id - """ - return await update_house(request, house_object, house_id, current_user) - - -@router.delete("/{house_id}") -async def api_delete_house(request: Request,house_id: str, current_user: PublicUser = Depends(get_current_user)): - """ - Delete House by ID - """ - - return await delete_house(request, house_id, current_user) diff --git a/src/services/houses.py b/src/services/houses.py deleted file mode 100644 index ecca7b68..00000000 --- a/src/services/houses.py +++ /dev/null @@ -1,157 +0,0 @@ -import json -from typing import List -from uuid import uuid4 -from pydantic import BaseModel -from src.services.users.users import PublicUser, User -from src.security.security import * -from fastapi import FastAPI, HTTPException, status, Request, Response, BackgroundTasks -from datetime import datetime - -#### Classes #################################################### - - -class House(BaseModel): - name: str - photo: str - description: str - email: str - org: str - - -class HouseInDB(House): - house_id: str - owners: List[str] - admins: List[str] - -#### Classes #################################################### - -# TODO : Add house photo upload and delete - -async def get_house(request: Request, house_id: str, current_user: PublicUser): - houses = request.app.db["houses"] - - house = houses.find_one({"house_id": house_id}) - - # verify house rights - await verify_house_rights(request,house_id, current_user, "read") - - if not house: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="House does not exist") - - house = House(**house) - return house - - -async def create_house(request: Request,house_object: House, current_user: PublicUser): - houses = request.app.db["houses"] - - # find if house already exists using name - isHouseAvailable = houses.find_one({"name": house_object.name}) - - if isHouseAvailable: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="House name already exists") - - # generate house_id with uuid4 - house_id = str(f"house_{uuid4()}") - - hasRoleRights = await verify_user_rights_with_roles(request, "create", current_user.user_id, house_id) - - if not hasRoleRights: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Roles : Insufficient rights to perform this action") - - house = HouseInDB(house_id=house_id, owners=[ - current_user.user_id], admins=[ - current_user.user_id], **house_object.dict()) - - house_in_db = houses.insert_one(house.dict()) - - if not house_in_db: - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Unavailable database") - - return house.dict() - - -async def update_house(request: Request,house_object: House, house_id: str, current_user: PublicUser): - - # verify house rights - await verify_house_rights(request,house_id, current_user, "update") - - houses = request.app.db["houses"] - - house = houses.find_one({"house_id": house_id}) - - if house: - # get owner value from house object database - owners = house["owners"] - admins = house["admins"] - - updated_house = HouseInDB( - house_id=house_id, owners=owners, admins=admins, **house_object.dict()) - - houses.update_one({"house_id": house_id}, {"$set": updated_house.dict()}) - - return HouseInDB(**updated_house.dict()) - - else: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="House does not exist") - - - - -async def delete_house(request: Request,house_id: str, current_user: PublicUser): - - # verify house rights - await verify_house_rights(request,house_id, current_user, "delete") - - houses = request.app.db["houses"] - - house = houses.find_one({"house_id": house_id}) - - if not house: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="House does not exist") - - isDeleted = houses.delete_one({"house_id": house_id}) - - if isDeleted: - return {"detail": "House deleted"} - else: - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Unavailable database") - - -async def get_houses(request: Request,page: int = 1, limit: int = 10): - houses = request.app.db["houses"] - # TODO : Get only houses that user is admin/has roles of - # get all houses from database - all_houses = houses.find().sort("name", 1).skip(10 * (page - 1)).limit(limit) - - return [json.loads(json.dumps(house, default=str)) for house in await all_houses.to_list(length=limit)] - - -#### Security #################################################### - -async def verify_house_rights(request: Request,house_id: str, current_user: PublicUser, action: str): - houses = request.app.db["houses"] - - house = houses.find_one({"house_id": house_id}) - - if not house: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="House does not exist") - - hasRoleRights = await verify_user_rights_with_roles(request,action, current_user.user_id, house_id) - isOwner = current_user.user_id in house["owners"] - - if not hasRoleRights and not isOwner: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail="Roles/Ownership : Insufficient rights to perform this action") - - return True - -#### Security #################################################### From 8af4dce92e59fe38100bcf1f102e39165ed063d1 Mon Sep 17 00:00:00 2001 From: swve Date: Sat, 25 Mar 2023 00:55:41 +0100 Subject: [PATCH 7/9] fix: remove faulty import --- app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app.py b/app.py index f62b2bba..58907580 100644 --- a/app.py +++ b/app.py @@ -3,7 +3,6 @@ import logging from fastapi import FastAPI, Request import re from config.config import LearnHouseConfig, get_learnhouse_config -from src.core.config.config import Settings, get_settings from src.core.events.events import shutdown_app, startup_app from src.main import global_router from fastapi.middleware.cors import CORSMiddleware From 316ab4025d10dde0dc37e4f51d2987512337011f Mon Sep 17 00:00:00 2001 From: swve Date: Sat, 25 Mar 2023 01:25:50 +0100 Subject: [PATCH 8/9] fix: remove houses faulty import --- src/services/roles/roles.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/roles/roles.py b/src/services/roles/roles.py index 67f34594..3893ff54 100644 --- a/src/services/roles/roles.py +++ b/src/services/roles/roles.py @@ -5,7 +5,6 @@ from pydantic import BaseModel from src.services.roles.schemas.roles import Role, RoleInDB from src.services.users.schemas.users import PublicUser, User from src.security.security import * -from src.services.houses import House from fastapi import HTTPException, status, Request from datetime import datetime From 27d2bc08f2d15b1d5d070d4dc73b5f3a5854e1a3 Mon Sep 17 00:00:00 2001 From: swve Date: Sun, 26 Mar 2023 14:04:33 +0200 Subject: [PATCH 9/9] feat: add support for selfHosted client-side --- config/config.yaml | 1 + .../_orgs/[orgslug]/(withmenu)/courses/page.tsx | 8 ++++---- front/middleware.ts | 8 ++++++-- front/services/config/config.ts | 15 +++++++-------- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/config/config.yaml b/config/config.yaml index 242fb31c..65532180 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -7,6 +7,7 @@ hosting_config: port: 443 ssl: true use_default_org: false + default_org: learnhouse allowed_origins: - https://learnhouse.app - https://learnhouse.io diff --git a/front/app/_orgs/[orgslug]/(withmenu)/courses/page.tsx b/front/app/_orgs/[orgslug]/(withmenu)/courses/page.tsx index ae143509..86706ad1 100644 --- a/front/app/_orgs/[orgslug]/(withmenu)/courses/page.tsx +++ b/front/app/_orgs/[orgslug]/(withmenu)/courses/page.tsx @@ -4,7 +4,7 @@ import { useRouter } from "next/navigation"; import React from "react"; import styled from "styled-components"; import { Title } from "@components/UI/Elements/Styles/Title"; -import { getAPIUrl, getBackendUrl, getUriWithOrg } from "@services/config/config"; +import { getAPIUrl, getBackendUrl, getSelfHostedOption, getUriWithOrg } from "@services/config/config"; import { deleteCourseFromBackend } from "@services/courses/courses"; import useSWR, { mutate } from "swr"; import { swrFetcher } from "@services/utils/requests"; @@ -25,7 +25,7 @@ const CoursesIndexPage = (params: any) => { function removeCoursePrefix(course_id: string) { return course_id.replace("course_", ""); } - + return ( <> @@ -44,8 +44,8 @@ const CoursesIndexPage = (params: any) => { <button style={{ backgroundColor: "red", border: "none" }} onClick={() => deleteCourses(course.course_id)}> Delete <Trash size={10}></Trash> </button> - <Link href={getUriWithOrg(orgslug,"") + "/course/" + removeCoursePrefix(course.course_id)}> - <Link href={getUriWithOrg(orgslug,"") + "/course/" + removeCoursePrefix(course.course_id) + "/edit"}> + <Link href={getUriWithOrg(orgslug, "/course/" + removeCoursePrefix(course.course_id))}> + <Link href={getUriWithOrg(orgslug, "/course/" + removeCoursePrefix(course.course_id) + "/edit")}> <button> Edit <Edit2 size={10}></Edit2> </button> diff --git a/front/middleware.ts b/front/middleware.ts index 15a48cbc..56fd95cd 100644 --- a/front/middleware.ts +++ b/front/middleware.ts @@ -20,8 +20,8 @@ export default function middleware(req: NextRequest) { const isSelfHosted = getSelfHostedOption(); const hostname = req.headers.get("host") || "learnhouse.app"; let currentHost = hostname.replace(".localhost:3000", ""); - - if (!isSelfHosted && currentHost === ("localhost:3000")) { + + if (!isSelfHosted && currentHost === "localhost:3000" && !url.pathname.startsWith("/organizations")) { // Redirect to error page if not self-hosted and on localhost const errorUrl = "/error"; return NextResponse.redirect(errorUrl, { status: 302 }); @@ -33,7 +33,11 @@ export default function middleware(req: NextRequest) { } if (url.pathname.startsWith("/organizations")) { + if (!isSelfHosted) { + currentHost = ""; + } url.pathname = url.pathname.replace("/organizations", `/organizations${currentHost}`).replace("localhost:3000", ""); + return NextResponse.rewrite(url); } diff --git a/front/services/config/config.ts b/front/services/config/config.ts index 08ee743a..b8352c0d 100644 --- a/front/services/config/config.ts +++ b/front/services/config/config.ts @@ -2,13 +2,12 @@ const LEARNHOUSE_API_URL = "http://localhost:1338/api/"; const LEARNHOUSE_BACKEND_URL = "http://localhost:1338/"; export const getAPIUrl = () => LEARNHOUSE_API_URL; - export const getBackendUrl = () => LEARNHOUSE_BACKEND_URL; - -export const getSelfHostedOption = () => false; +export const getSelfHostedOption = () => (process.env.NEXT_PUBLIC_LEARNHOUSE_SELF_HOSTED === "true" ? true : false); export const getUriWithOrg = (orgslug: string, path: string) => { const selfHosted = getSelfHostedOption(); + if (selfHosted) { return `http://localhost:3000${path}`; } @@ -20,16 +19,16 @@ export const getOrgFromUri = () => { if (selfHosted) { getDefaultOrg(); } else { - const hostname = window.location.hostname; - // get the orgslug from the hostname - const orgslug = hostname.split(".")[0]; - return orgslug; + if (typeof window !== "undefined") { + const hostname = window.location.hostname; + return hostname.replace(".localhost:3000", ""); + } } }; export const getDefaultOrg = () => { const selfHosted = getSelfHostedOption(); if (selfHosted) { - return "test"; + return process.env.NEXT_PUBLIC_LEARNHOUSE_DEFAULT_ORG; } };