From a9dea09a7ce3af41cafba103d99c304815bedd9b Mon Sep 17 00:00:00 2001 From: John Grubba Date: Mon, 29 Jul 2024 11:11:32 +0200 Subject: [PATCH] Extensions --- .gitignore | 3 +- docker-compose.dev.yml | 2 +- docs/advanced/extensions.md | 77 +++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + src/api/main.py | 39 +++++++++++++++++++ 5 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 docs/advanced/extensions.md diff --git a/.gitignore b/.gitignore index bb438b6..a754ed7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ **config.json **.pyc -**.env** \ No newline at end of file +**.env** +**extensions/ \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 74e118a..28ccbe9 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -13,7 +13,7 @@ services: - ./config:/src/app/config - ./src:/src/app command: | - bash -c 'uvicorn api.main:app --reload --host 0.0.0.0 --port 80' + bash -c 'uvicorn api.main:app --reload --log-level debug --host 0.0.0.0 --port 80' depends_on: - db - redis diff --git a/docs/advanced/extensions.md b/docs/advanced/extensions.md new file mode 100644 index 0000000..57c6469 --- /dev/null +++ b/docs/advanced/extensions.md @@ -0,0 +1,77 @@ +Since the Goal of EZAuth is to be as extensive as possible, it is possible to create your own extensions to add functionality to EZAuth. + +## Preparation + +To allow Extensions, you need to create a new folder called `extensions/` in the `src/` directory, if it doesn't already exist. This folder is not tracked by git, so you can safely add your own extensions without the risk of losing them when updating the repository. + +```sh +mkdir -p src/extensions +``` + +!!! Tip "EZAuth Developer Mode" + To also enjoy a greater developement experience, you can start EZAuth in [Development Mode](https://github.com/JohnGrubba/ezauth?tab=readme-ov-file#developement). This will allow you to see changes in your extensions without restarting the server. + It will also show you more detailed error messages, which can be helpful when developing extensions. + + + +## Creating an Extension + +!!! Danger "Extensions can break **EVERYTHING (including all your userdata)**" + Since Extensions are a really advanced feature, they are not recommended to be developed by beginners. However, if you are an experienced developer, you can create your own extensions. + +1. New Folder for the Extension + + Inside the `extensions/` directory, create a new folder for your extension. The name of the folder should be the name of your extension. We force a folder, to make it easier for you to structure your extension and keep an overview of all the installed extensions. We use the name `my_extension` for this example. + + ```sh + mkdir src/extensions/my_extension + ``` + +2. Make the Extension Loadable + + To make EZAuth recognize your extension as a valid one, add a `__init__.py` file to your newly created extension folder. This file **must** be there and export a `router` which is a FastAPI Router. + + Depending on the structure of your Extension, you can either write the whole extension into the `__init__.py` file or import only the router from another file. + + !!! Tip "Imports in Extension Files" + Be careful when doing imports in extensions, as by just importing other files using `.myextension` won't work (importlib can't find the module). You have to use the full import path to the file starting with `extensions.my_extension.` -> E.g. `extensions.my_extension.myextension` + + ```python title="src/extensions/my_extension/myextension.py" + from fastapi import APIRouter + + router = APIRouter( + prefix="/test", + tags=["Test Extension"] + ) + + @router.get("") + async def test(): + """ + # Test Endpoint + """ + pass + + ``` + + ```python title="src/extensions/my_extension/__init__.py" + from extensions.my_extension.myextension import router + ``` + +3. Provide some information about the Extension + + To make it easier for users to understand what your extension does, you can provide a `README.md` file in your extension folder. This file can contain information about the extension, how to use it, and what it does. + + ```md title="src/extensions/my_extension/README.md" + # My Extension + + This is a test extension for EZAuth. + ``` + + This is especially useful if you want to share your extension with others. + + +## Downloading Extensions + +If you want to use an extension that someone else has created, you can download it from a repository and place it in the `extensions/` directory. The extension should be structured as described above. + +Stay tuned for a repository of extensions that you can use to enhance your EZAuth experience. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 36511fe..1dbbcbe 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,6 +16,7 @@ nav: - Further Customization: advanced/further_custom.md - OAuth: advanced/oauth.md - SSL / HTTPS: advanced/ssl.md + - Extensions: advanced/extensions.md theme: name: material logo: "ezauth_logo.png" diff --git a/src/api/main.py b/src/api/main.py index 1d84624..ac72793 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -9,9 +9,13 @@ from api.oauth_providers import router as oauthRouter from api.sessions import router as sessionsRouter import logging +import os +import importlib +import importlib.util from tools import SecurityConfig logging.basicConfig(format="%(message)s", level=logging.INFO, force=True) +logger = logging.getLogger("uvicorn") app = FastAPI( title="EZAuth API", @@ -50,3 +54,38 @@ async def up(): app.include_router(twofactorRouter) app.include_router(oauthRouter) app.include_router(internalRouter) + + +def load_extensions(): + # Extension Loading + extensions_dir = "/src/app/extensions/" + if not os.path.exists(extensions_dir): + return + modules = [] + for item in os.listdir(extensions_dir): + item_path = os.path.join(extensions_dir, item) + init_file = os.path.join(item_path, "__init__.py") + if os.path.isdir(item_path) and os.path.isfile(init_file): + spec = importlib.util.spec_from_file_location(item, init_file) + module = importlib.util.module_from_spec(spec) + try: + spec.loader.exec_module(module) + except Exception as e: + logger.error(f"Failed to load extension {item}: {e}") + if logger.level == logging.INFO: + raise e + continue + modules.append([spec, module]) + + for spec, module in modules: + app.include_router(module.router) + + logger.info( + "\u001b[32m-> Loaded Extensions: " + + ", ".join([module.__name__ for spec, module in modules]) + + "\u001b[0m" + ) + + +load_extensions() +logger.info("\u001b[32m--- API Startup Done ---\u001b[0m")