From 877804c0ffdb227fc254380770b39f8825e7082e Mon Sep 17 00:00:00 2001 From: jebus Date: Tue, 14 Apr 2026 21:59:29 +1200 Subject: [PATCH] [Feature] Path prefixes (#369) Co-authored-by: Tim Lorsbach Reviewed-on: https://git.envipath.com/enviPath/enviPy/pulls/369 --- .env.prod.example | 43 +++++++++++++++++++++++++++++++++++++---- Dockerfile | 1 + envipath/settings.py | 34 +++++++++++++++++++++++--------- envipath/urls.py | 19 +++++++++++------- epauth/urls.py | 4 ++-- epauth/views.py | 46 +++++++++++++++++++------------------------- epdb/views.py | 30 ++++++++++++++--------------- 7 files changed, 114 insertions(+), 63 deletions(-) diff --git a/.env.prod.example b/.env.prod.example index 217b9c91..d80d038e 100644 --- a/.env.prod.example +++ b/.env.prod.example @@ -3,10 +3,20 @@ EP_DATA_DIR= ALLOWED_HOSTS= DEBUG= LOG_LEVEL= +MODEL_BUILDING_ENABLED= +APPLICABILITY_DOMAIN_ENABLED= ENVIFORMER_PRESENT= -FLAG_CELERY_PRESENT= -SERVER_URL= +ENVIFORMER_DEVICE= PLUGINS_ENABLED= +SERVER_URL= +SERVER_PATH= +ADMIN_APPROVAL_REQUIRED= +REGISTRATION_MANDATORY= +LOG_DIR= +# Celery +FLAG_CELERY_PRESENT= +CELERY_BROKER_URL= +CELERY_RESULT_BACKEND= # DB POSTGRES_SERVICE_NAME= POSTGRES_DB= @@ -16,5 +26,30 @@ POSTGRES_PORT= # MAIL EMAIL_HOST_USER= EMAIL_HOST_PASSWORD= -# MATOMO -MATOMO_SITE_ID +DEFAULT_FROM_EMAIL= +SERVER_EMAIL= +# SENTRY +SENTRY_ENABLED= +SENTRY_DSN= +SENTRY_ENVIRONMENT= +# MS ENTRA +MS_ENTRA_ENABLED= +MS_CLIENT_ID= +MS_CLIENT_SECRET= +MS_TENANT_ID= +MS_REDIRECT_URI= +MS_SCOPES= +# Tenant +TENANT= +EPDB_PACKAGE_MODEL= +# Captcha +CAP_ENABLED= +CAP_API_BASE= +CAP_SITE_KEY= +CAP_SECRET_KEY= +# QUARKUS (JAVA) +ENVIRULE_ENABLED= +FINGERPRINT_URL= +# Biotransformer +BIOTRANSFORMER_ENABLED= +BIOTRANSFORMER_URL= diff --git a/Dockerfile b/Dockerfile index 5cb8cd24..e8e99c4e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -71,6 +71,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libxrender1 \ libxext6 \ libfontconfig1 \ + nano \ && rm -rf /var/lib/apt/lists/* RUN useradd -ms /bin/bash django diff --git a/envipath/settings.py b/envipath/settings.py index f551a7bc..29a7f694 100644 --- a/envipath/settings.py +++ b/envipath/settings.py @@ -191,11 +191,21 @@ ADMIN_APPROVAL_REQUIRED = os.environ.get("ADMIN_APPROVAL_REQUIRED", "False") == # SESAME_MAX_AGE = 300 # # TODO set to "home" # LOGIN_REDIRECT_URL = "/" + + +SERVER_HOST = os.environ.get("SERVER_URL", "http://localhost:8000") +SERVER_PATH = os.environ.get("SERVER_PATH", "") + +SERVER_URL = SERVER_HOST +if SERVER_PATH: + SERVER_URL = os.path.join(SERVER_HOST, SERVER_PATH) + + LOGIN_URL = "/login/" +if SERVER_PATH: + LOGIN_URL = f"/{SERVER_PATH}/login/" -SERVER_URL = os.environ.get("SERVER_URL", "http://localhost:8000") - -CSRF_TRUSTED_ORIGINS = [SERVER_URL] +CSRF_TRUSTED_ORIGINS = [SERVER_HOST] AMBIT_URL = "http://localhost:9001" DEFAULT_VALUES = {"description": "no description"} @@ -229,6 +239,8 @@ PAGINATION_MAX_PER_PAGE_SIZE = int( STATIC_ROOT = STATIC_DIR STATIC_URL = "/static/" +if SERVER_PATH: + STATIC_URL = f"/{SERVER_PATH}/static/" # Where the sources are stored... STATICFILES_DIRS = (BASE_DIR / "static",) @@ -331,10 +343,11 @@ DEFAULT_MODEL_THRESHOLD = 0.25 # Loading Plugins PLUGINS_ENABLED = os.environ.get("PLUGINS_ENABLED", "False") == "True" -BASE_PLUGINS = [ - "pepper.PEPPER", - "biotransformer.Biotransformer", -] +BASE_PLUGINS = os.environ.get("BASE_PLUGINS", None) +if BASE_PLUGINS: + BASE_PLUGINS = BASE_PLUGINS.split(",") +else: + BASE_PLUGINS = [] CLASSIFIER_PLUGINS = {} PROPERTY_PLUGINS = {} @@ -387,7 +400,6 @@ LOGIN_EXEMPT_URLS = [ "/o/userinfo/", "/password_reset/", "/reset/", - "/microsoft/", "/terms", "/privacy", "/cookie-policy", @@ -396,8 +408,13 @@ LOGIN_EXEMPT_URLS = [ "/careers", "/cite", "/legal", + "/entra/", + "/auth/", ] +if SERVER_PATH: + LOGIN_EXEMPT_URLS = [f"/{SERVER_PATH}{x}" for x in LOGIN_EXEMPT_URLS] + # MS AD/Entra MS_ENTRA_ENABLED = os.environ.get("MS_ENTRA_ENABLED", "False") == "True" if MS_ENTRA_ENABLED: @@ -424,5 +441,4 @@ CAP_SECRET_KEY = os.environ.get("CAP_SECRET_KEY", None) BIOTRANSFORMER_ENABLED = os.environ.get("BIOTRANSFORMER_ENABLED", "False") == "True" FLAGS["BIOTRANSFORMER"] = BIOTRANSFORMER_ENABLED if BIOTRANSFORMER_ENABLED: - INSTALLED_APPS.append("biotransformer") BIOTRANSFORMER_URL = os.environ.get("BIOTRANSFORMER_URL", None) diff --git a/envipath/urls.py b/envipath/urls.py index 799d89bd..dc46f0d3 100644 --- a/envipath/urls.py +++ b/envipath/urls.py @@ -21,19 +21,24 @@ from django.urls import include, path from .api import api_v1, api_legacy +PATH_PREFIX = s.SERVER_PATH +if PATH_PREFIX and not PATH_PREFIX.endswith("/"): + PATH_PREFIX += "/" + + urlpatterns = [ - path("", include("epdb.urls")), - path("admin/", admin.site.urls), - path("api/v1/", api_v1.urls), - path("api/legacy/", api_legacy.urls), - path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")), + path(f"{PATH_PREFIX}", include("epdb.urls")), + path(f"{PATH_PREFIX}admin/", admin.site.urls), + path(f"{PATH_PREFIX}api/v1/", api_v1.urls), + path(f"{PATH_PREFIX}api/legacy/", api_legacy.urls), + path(f"{PATH_PREFIX}o/", include("oauth2_provider.urls", namespace="oauth2_provider")), ] if "migration" in s.INSTALLED_APPS: - urlpatterns.append(path("", include("migration.urls"))) + urlpatterns.append(path(f"{PATH_PREFIX}", include("migration.urls"))) if s.MS_ENTRA_ENABLED: - urlpatterns.append(path("", include("epauth.urls"))) + urlpatterns.append(path(f"{PATH_PREFIX}", include("epauth.urls"))) # Custom error handlers handler400 = "epdb.views.handler400" diff --git a/epauth/urls.py b/epauth/urls.py index 853e7add..c251d799 100644 --- a/epauth/urls.py +++ b/epauth/urls.py @@ -3,6 +3,6 @@ from django.urls import path from . import views urlpatterns = [ - path("microsoft/login/", views.microsoft_login, name="microsoft_login"), - path("microsoft/callback/", views.microsoft_callback, name="microsoft_callback"), + path("entra/login/", views.entra_login, name="entra_login"), + path("auth/redirect/", views.entra_callback, name="entra_callback"), ] diff --git a/epauth/views.py b/epauth/views.py index f6253cb1..e73a3ff9 100644 --- a/epauth/views.py +++ b/epauth/views.py @@ -7,27 +7,26 @@ from django.contrib.auth import get_user_model from epdb.logic import UserManager -def microsoft_login(request): +def entra_login(request): msal_app = msal.ConfidentialClientApplication( client_id=s.MS_ENTRA_CLIENT_ID, client_credential=s.MS_ENTRA_CLIENT_SECRET, - authority=s.MS_ENTRA_AUTHORITY + authority=s.MS_ENTRA_AUTHORITY, ) flow = msal_app.initiate_auth_code_flow( - scopes=s.MS_ENTRA_SCOPES, - redirect_uri=s.MS_ENTRA_REDIRECT_URI + scopes=s.MS_ENTRA_SCOPES, redirect_uri=s.MS_ENTRA_REDIRECT_URI ) request.session["msal_auth_flow"] = flow return redirect(flow["auth_uri"]) -def microsoft_callback(request): +def entra_callback(request): msal_app = msal.ConfidentialClientApplication( client_id=s.MS_ENTRA_CLIENT_ID, client_credential=s.MS_ENTRA_CLIENT_SECRET, - authority=s.MS_ENTRA_AUTHORITY + authority=s.MS_ENTRA_AUTHORITY, ) flow = request.session.pop("msal_auth_flow", None) @@ -37,30 +36,25 @@ def microsoft_callback(request): # Acquire token using the flow and callback request result = msal_app.acquire_token_by_auth_code_flow(flow, request.GET) - if "access_token" in result: - # Optional: Fetch user info from Microsoft Graph - import requests - resp = requests.get( - "https://graph.microsoft.com/v1.0/me", - headers={"Authorization": f"Bearer {result['access_token']}"} - ) - user_info = resp.json() + claims = result["id_token_claims"] - user_name = user_info["displayName"] - user_email = user_info["mail"] - user_oid = user_info["id"] + user_name = claims["name"] + user_email = claims["emailaddress"] + user_oid = claims["oid"] - # Get implementing class - User = get_user_model() + # Get implementing class + User = get_user_model() - if User.objects.filter(uuid=user_oid).exists(): - login(request, User.objects.get(uuid=user_oid)) - else: - u = UserManager.create_user(user_name, user_email, None, uuid=user_oid, is_active=True) - login(request, u) + if User.objects.filter(uuid=user_oid).exists(): + u = User.objects.get(uuid=user_oid) - # TODO Group Sync + if u.username != user_name: + u.username = user_name + u.save() - return redirect("/") + else: + u = UserManager.create_user(user_name, user_email, None, uuid=user_oid, is_active=True) + + login(request, u) return redirect("/") # Handle errors diff --git a/epdb/views.py b/epdb/views.py index 917f2fa8..88b59296 100644 --- a/epdb/views.py +++ b/epdb/views.py @@ -556,7 +556,7 @@ def packages(request): # Context for paginated template context["entity_type"] = "package" - context["api_endpoint"] = "/api/v1/packages/" + context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/packages/" context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE context["list_title"] = "packages" @@ -614,7 +614,7 @@ def compounds(request): # Context for paginated template context["entity_type"] = "compound" - context["api_endpoint"] = "/api/v1/compounds/" + context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/compounds/" context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE context["list_mode"] = "tabbed" context["list_title"] = "compounds" @@ -643,7 +643,7 @@ def rules(request): # Context for paginated template context["entity_type"] = "rule" - context["api_endpoint"] = "/api/v1/rules/" + context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/rules/" context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE context["list_title"] = "rules" @@ -671,7 +671,7 @@ def reactions(request): # Context for paginated template context["entity_type"] = "reaction" - context["api_endpoint"] = "/api/v1/reactions/" + context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/reactions/" context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE context["list_title"] = "reactions" @@ -699,7 +699,7 @@ def pathways(request): # Context for paginated template context["entity_type"] = "pathway" - context["api_endpoint"] = "/api/v1/pathways/" + context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/pathways/" context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE context["list_title"] = "pathways" @@ -729,7 +729,7 @@ def scenarios(request): # Context for paginated template context["entity_type"] = "scenario" - context["api_endpoint"] = "/api/v1/scenarios/" + context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/scenarios/" context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE context["list_title"] = "scenarios" @@ -789,7 +789,7 @@ def models(request): # Context for paginated template context["entity_type"] = "model" - context["api_endpoint"] = "/api/v1/models/" + context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/models/" context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE context["list_title"] = "models" @@ -871,7 +871,7 @@ def package_models(request, package_uuid): context["object_type"] = "model" context["breadcrumbs"] = breadcrumbs(current_package, "model") context["entity_type"] = "model" - context["api_endpoint"] = f"/api/v1/package/{current_package.uuid}/model/" + context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/package/{current_package.uuid}/model/" context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE context["list_title"] = "models" @@ -1341,7 +1341,7 @@ def package_compounds(request, package_uuid): context["object_type"] = "compound" context["breadcrumbs"] = breadcrumbs(current_package, "compound") context["entity_type"] = "compound" - context["api_endpoint"] = f"/api/v1/package/{current_package.uuid}/compound/" + context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/package/{current_package.uuid}/compound/" context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE context["list_mode"] = "tabbed" context["list_title"] = "compounds" @@ -1494,7 +1494,7 @@ def package_compound_structures(request, package_uuid, compound_uuid): context["entity_type"] = "structure" context["page_title"] = f"{current_compound.get_name()} - Structures" context["api_endpoint"] = ( - f"/api/v1/package/{current_package.uuid}/compound/{current_compound.uuid}/structure/" + f"{s.SERVER_PATH}/api/v1/package/{current_package.uuid}/compound/{current_compound.uuid}/structure/" ) context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE context["compound"] = current_compound @@ -1657,7 +1657,7 @@ def package_rules(request, package_uuid): context["object_type"] = "rule" context["breadcrumbs"] = breadcrumbs(current_package, "rule") context["entity_type"] = "rule" - context["api_endpoint"] = f"/api/v1/package/{current_package.uuid}/rule/" + context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/package/{current_package.uuid}/rule/" context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE context["list_title"] = "rules" @@ -1865,7 +1865,7 @@ def package_reactions(request, package_uuid): context["object_type"] = "reaction" context["breadcrumbs"] = breadcrumbs(current_package, "reaction") context["entity_type"] = "reaction" - context["api_endpoint"] = f"/api/v1/package/{current_package.uuid}/reaction/" + context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/package/{current_package.uuid}/reaction/" context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE context["list_title"] = "reactions" @@ -2015,7 +2015,7 @@ def package_pathways(request, package_uuid): context["object_type"] = "pathway" context["breadcrumbs"] = breadcrumbs(current_package, "pathway") context["entity_type"] = "pathway" - context["api_endpoint"] = f"/api/v1/package/{current_package.uuid}/pathway/" + context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/package/{current_package.uuid}/pathway/" context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE context["list_title"] = "pathways" @@ -2591,7 +2591,7 @@ def package_scenarios(request, package_uuid): context["object_type"] = "scenario" context["breadcrumbs"] = breadcrumbs(current_package, "scenario") context["entity_type"] = "scenario" - context["api_endpoint"] = f"/api/v1/package/{current_package.uuid}/scenario/" + context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/package/{current_package.uuid}/scenario/" context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE context["list_title"] = "scenarios" @@ -2940,7 +2940,7 @@ def settings(request): # Context for paginated template context["entity_type"] = "setting" - context["api_endpoint"] = "/api/v1/settings/" + context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/settings/" context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE context["list_title"] = "settings" context["list_mode"] = "combined"