[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=
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=

View File

@ -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

View File

@ -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)

View File

@ -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"

View File

@ -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"),
]

View File

@ -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

View File

@ -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"