[Feature] Path prefixes (#369)

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#369
This commit is contained in:
2026-04-14 21:59:29 +12:00
parent 964574c700
commit 877804c0ff
7 changed files with 114 additions and 63 deletions

View File

@ -3,10 +3,20 @@ EP_DATA_DIR=
ALLOWED_HOSTS= ALLOWED_HOSTS=
DEBUG= DEBUG=
LOG_LEVEL= LOG_LEVEL=
MODEL_BUILDING_ENABLED=
APPLICABILITY_DOMAIN_ENABLED=
ENVIFORMER_PRESENT= ENVIFORMER_PRESENT=
FLAG_CELERY_PRESENT= ENVIFORMER_DEVICE=
SERVER_URL=
PLUGINS_ENABLED= PLUGINS_ENABLED=
SERVER_URL=
SERVER_PATH=
ADMIN_APPROVAL_REQUIRED=
REGISTRATION_MANDATORY=
LOG_DIR=
# Celery
FLAG_CELERY_PRESENT=
CELERY_BROKER_URL=
CELERY_RESULT_BACKEND=
# DB # DB
POSTGRES_SERVICE_NAME= POSTGRES_SERVICE_NAME=
POSTGRES_DB= POSTGRES_DB=
@ -16,5 +26,30 @@ POSTGRES_PORT=
# MAIL # MAIL
EMAIL_HOST_USER= EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD= EMAIL_HOST_PASSWORD=
# MATOMO DEFAULT_FROM_EMAIL=
MATOMO_SITE_ID 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=

View File

@ -71,6 +71,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libxrender1 \ libxrender1 \
libxext6 \ libxext6 \
libfontconfig1 \ libfontconfig1 \
nano \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN useradd -ms /bin/bash django RUN useradd -ms /bin/bash django

View File

@ -191,11 +191,21 @@ ADMIN_APPROVAL_REQUIRED = os.environ.get("ADMIN_APPROVAL_REQUIRED", "False") ==
# SESAME_MAX_AGE = 300 # SESAME_MAX_AGE = 300
# # TODO set to "home" # # TODO set to "home"
# LOGIN_REDIRECT_URL = "/" # 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/" 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_HOST]
CSRF_TRUSTED_ORIGINS = [SERVER_URL]
AMBIT_URL = "http://localhost:9001" AMBIT_URL = "http://localhost:9001"
DEFAULT_VALUES = {"description": "no description"} DEFAULT_VALUES = {"description": "no description"}
@ -229,6 +239,8 @@ PAGINATION_MAX_PER_PAGE_SIZE = int(
STATIC_ROOT = STATIC_DIR STATIC_ROOT = STATIC_DIR
STATIC_URL = "/static/" STATIC_URL = "/static/"
if SERVER_PATH:
STATIC_URL = f"/{SERVER_PATH}/static/"
# Where the sources are stored... # Where the sources are stored...
STATICFILES_DIRS = (BASE_DIR / "static",) STATICFILES_DIRS = (BASE_DIR / "static",)
@ -331,10 +343,11 @@ DEFAULT_MODEL_THRESHOLD = 0.25
# Loading Plugins # Loading Plugins
PLUGINS_ENABLED = os.environ.get("PLUGINS_ENABLED", "False") == "True" PLUGINS_ENABLED = os.environ.get("PLUGINS_ENABLED", "False") == "True"
BASE_PLUGINS = [ BASE_PLUGINS = os.environ.get("BASE_PLUGINS", None)
"pepper.PEPPER", if BASE_PLUGINS:
"biotransformer.Biotransformer", BASE_PLUGINS = BASE_PLUGINS.split(",")
] else:
BASE_PLUGINS = []
CLASSIFIER_PLUGINS = {} CLASSIFIER_PLUGINS = {}
PROPERTY_PLUGINS = {} PROPERTY_PLUGINS = {}
@ -387,7 +400,6 @@ LOGIN_EXEMPT_URLS = [
"/o/userinfo/", "/o/userinfo/",
"/password_reset/", "/password_reset/",
"/reset/", "/reset/",
"/microsoft/",
"/terms", "/terms",
"/privacy", "/privacy",
"/cookie-policy", "/cookie-policy",
@ -396,8 +408,13 @@ LOGIN_EXEMPT_URLS = [
"/careers", "/careers",
"/cite", "/cite",
"/legal", "/legal",
"/entra/",
"/auth/",
] ]
if SERVER_PATH:
LOGIN_EXEMPT_URLS = [f"/{SERVER_PATH}{x}" for x in LOGIN_EXEMPT_URLS]
# MS AD/Entra # MS AD/Entra
MS_ENTRA_ENABLED = os.environ.get("MS_ENTRA_ENABLED", "False") == "True" MS_ENTRA_ENABLED = os.environ.get("MS_ENTRA_ENABLED", "False") == "True"
if MS_ENTRA_ENABLED: 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" BIOTRANSFORMER_ENABLED = os.environ.get("BIOTRANSFORMER_ENABLED", "False") == "True"
FLAGS["BIOTRANSFORMER"] = BIOTRANSFORMER_ENABLED FLAGS["BIOTRANSFORMER"] = BIOTRANSFORMER_ENABLED
if BIOTRANSFORMER_ENABLED: if BIOTRANSFORMER_ENABLED:
INSTALLED_APPS.append("biotransformer")
BIOTRANSFORMER_URL = os.environ.get("BIOTRANSFORMER_URL", None) BIOTRANSFORMER_URL = os.environ.get("BIOTRANSFORMER_URL", None)

View File

@ -21,19 +21,24 @@ from django.urls import include, path
from .api import api_v1, api_legacy from .api import api_v1, api_legacy
PATH_PREFIX = s.SERVER_PATH
if PATH_PREFIX and not PATH_PREFIX.endswith("/"):
PATH_PREFIX += "/"
urlpatterns = [ urlpatterns = [
path("", include("epdb.urls")), path(f"{PATH_PREFIX}", include("epdb.urls")),
path("admin/", admin.site.urls), path(f"{PATH_PREFIX}admin/", admin.site.urls),
path("api/v1/", api_v1.urls), path(f"{PATH_PREFIX}api/v1/", api_v1.urls),
path("api/legacy/", api_legacy.urls), path(f"{PATH_PREFIX}api/legacy/", api_legacy.urls),
path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")), path(f"{PATH_PREFIX}o/", include("oauth2_provider.urls", namespace="oauth2_provider")),
] ]
if "migration" in s.INSTALLED_APPS: 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: if s.MS_ENTRA_ENABLED:
urlpatterns.append(path("", include("epauth.urls"))) urlpatterns.append(path(f"{PATH_PREFIX}", include("epauth.urls")))
# Custom error handlers # Custom error handlers
handler400 = "epdb.views.handler400" handler400 = "epdb.views.handler400"

View File

@ -3,6 +3,6 @@ from django.urls import path
from . import views from . import views
urlpatterns = [ urlpatterns = [
path("microsoft/login/", views.microsoft_login, name="microsoft_login"), path("entra/login/", views.entra_login, name="entra_login"),
path("microsoft/callback/", views.microsoft_callback, name="microsoft_callback"), path("auth/redirect/", views.entra_callback, name="entra_callback"),
] ]

View File

@ -7,27 +7,26 @@ from django.contrib.auth import get_user_model
from epdb.logic import UserManager from epdb.logic import UserManager
def microsoft_login(request): def entra_login(request):
msal_app = msal.ConfidentialClientApplication( msal_app = msal.ConfidentialClientApplication(
client_id=s.MS_ENTRA_CLIENT_ID, client_id=s.MS_ENTRA_CLIENT_ID,
client_credential=s.MS_ENTRA_CLIENT_SECRET, client_credential=s.MS_ENTRA_CLIENT_SECRET,
authority=s.MS_ENTRA_AUTHORITY authority=s.MS_ENTRA_AUTHORITY,
) )
flow = msal_app.initiate_auth_code_flow( flow = msal_app.initiate_auth_code_flow(
scopes=s.MS_ENTRA_SCOPES, scopes=s.MS_ENTRA_SCOPES, redirect_uri=s.MS_ENTRA_REDIRECT_URI
redirect_uri=s.MS_ENTRA_REDIRECT_URI
) )
request.session["msal_auth_flow"] = flow request.session["msal_auth_flow"] = flow
return redirect(flow["auth_uri"]) return redirect(flow["auth_uri"])
def microsoft_callback(request): def entra_callback(request):
msal_app = msal.ConfidentialClientApplication( msal_app = msal.ConfidentialClientApplication(
client_id=s.MS_ENTRA_CLIENT_ID, client_id=s.MS_ENTRA_CLIENT_ID,
client_credential=s.MS_ENTRA_CLIENT_SECRET, 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) flow = request.session.pop("msal_auth_flow", None)
@ -37,30 +36,25 @@ def microsoft_callback(request):
# Acquire token using the flow and callback request # Acquire token using the flow and callback request
result = msal_app.acquire_token_by_auth_code_flow(flow, request.GET) result = msal_app.acquire_token_by_auth_code_flow(flow, request.GET)
if "access_token" in result: claims = result["id_token_claims"]
# 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()
user_name = user_info["displayName"] user_name = claims["name"]
user_email = user_info["mail"] user_email = claims["emailaddress"]
user_oid = user_info["id"] user_oid = claims["oid"]
# Get implementing class # Get implementing class
User = get_user_model() User = get_user_model()
if User.objects.filter(uuid=user_oid).exists(): if User.objects.filter(uuid=user_oid).exists():
login(request, User.objects.get(uuid=user_oid)) u = User.objects.get(uuid=user_oid)
if u.username != user_name:
u.username = user_name
u.save()
else: else:
u = UserManager.create_user(user_name, user_email, None, uuid=user_oid, is_active=True) u = UserManager.create_user(user_name, user_email, None, uuid=user_oid, is_active=True)
login(request, u) login(request, u)
# TODO Group Sync
return redirect("/")
return redirect("/") # Handle errors return redirect("/") # Handle errors

View File

@ -556,7 +556,7 @@ def packages(request):
# Context for paginated template # Context for paginated template
context["entity_type"] = "package" 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["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "packages" context["list_title"] = "packages"
@ -614,7 +614,7 @@ def compounds(request):
# Context for paginated template # Context for paginated template
context["entity_type"] = "compound" 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["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_mode"] = "tabbed" context["list_mode"] = "tabbed"
context["list_title"] = "compounds" context["list_title"] = "compounds"
@ -643,7 +643,7 @@ def rules(request):
# Context for paginated template # Context for paginated template
context["entity_type"] = "rule" 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["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "rules" context["list_title"] = "rules"
@ -671,7 +671,7 @@ def reactions(request):
# Context for paginated template # Context for paginated template
context["entity_type"] = "reaction" 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["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "reactions" context["list_title"] = "reactions"
@ -699,7 +699,7 @@ def pathways(request):
# Context for paginated template # Context for paginated template
context["entity_type"] = "pathway" 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["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "pathways" context["list_title"] = "pathways"
@ -729,7 +729,7 @@ def scenarios(request):
# Context for paginated template # Context for paginated template
context["entity_type"] = "scenario" 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["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "scenarios" context["list_title"] = "scenarios"
@ -789,7 +789,7 @@ def models(request):
# Context for paginated template # Context for paginated template
context["entity_type"] = "model" 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["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "models" context["list_title"] = "models"
@ -871,7 +871,7 @@ def package_models(request, package_uuid):
context["object_type"] = "model" context["object_type"] = "model"
context["breadcrumbs"] = breadcrumbs(current_package, "model") context["breadcrumbs"] = breadcrumbs(current_package, "model")
context["entity_type"] = "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["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "models" context["list_title"] = "models"
@ -1341,7 +1341,7 @@ def package_compounds(request, package_uuid):
context["object_type"] = "compound" context["object_type"] = "compound"
context["breadcrumbs"] = breadcrumbs(current_package, "compound") context["breadcrumbs"] = breadcrumbs(current_package, "compound")
context["entity_type"] = "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["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_mode"] = "tabbed" context["list_mode"] = "tabbed"
context["list_title"] = "compounds" context["list_title"] = "compounds"
@ -1494,7 +1494,7 @@ def package_compound_structures(request, package_uuid, compound_uuid):
context["entity_type"] = "structure" context["entity_type"] = "structure"
context["page_title"] = f"{current_compound.get_name()} - Structures" context["page_title"] = f"{current_compound.get_name()} - Structures"
context["api_endpoint"] = ( 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["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["compound"] = current_compound context["compound"] = current_compound
@ -1657,7 +1657,7 @@ def package_rules(request, package_uuid):
context["object_type"] = "rule" context["object_type"] = "rule"
context["breadcrumbs"] = breadcrumbs(current_package, "rule") context["breadcrumbs"] = breadcrumbs(current_package, "rule")
context["entity_type"] = "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["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "rules" context["list_title"] = "rules"
@ -1865,7 +1865,7 @@ def package_reactions(request, package_uuid):
context["object_type"] = "reaction" context["object_type"] = "reaction"
context["breadcrumbs"] = breadcrumbs(current_package, "reaction") context["breadcrumbs"] = breadcrumbs(current_package, "reaction")
context["entity_type"] = "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["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "reactions" context["list_title"] = "reactions"
@ -2015,7 +2015,7 @@ def package_pathways(request, package_uuid):
context["object_type"] = "pathway" context["object_type"] = "pathway"
context["breadcrumbs"] = breadcrumbs(current_package, "pathway") context["breadcrumbs"] = breadcrumbs(current_package, "pathway")
context["entity_type"] = "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["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "pathways" context["list_title"] = "pathways"
@ -2591,7 +2591,7 @@ def package_scenarios(request, package_uuid):
context["object_type"] = "scenario" context["object_type"] = "scenario"
context["breadcrumbs"] = breadcrumbs(current_package, "scenario") context["breadcrumbs"] = breadcrumbs(current_package, "scenario")
context["entity_type"] = "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["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "scenarios" context["list_title"] = "scenarios"
@ -2940,7 +2940,7 @@ def settings(request):
# Context for paginated template # Context for paginated template
context["entity_type"] = "setting" 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["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "settings" context["list_title"] = "settings"
context["list_mode"] = "combined" context["list_mode"] = "combined"