from flask import request, url_for, current_app from urllib.parse import urlsplit, urlunsplit import re VISIBLE_PREFIXES = ('/admin', '/api', '/chat-client') def _normalize_prefix(raw_prefix: str) -> str: """Normalize config prefix to internal form '/admin' or '' if not set.""" if not raw_prefix: return '' s = str(raw_prefix).strip() if not s: return '' # remove leading/trailing slashes, then add single leading slash s = s.strip('/') if not s: return '' return f"/{s}" def _get_config_prefix() -> str: """Return normalized prefix from config EVEAI_APP_PREFIX (config-first).""" try: cfg_val = (current_app.config.get('EVEAI_APP_PREFIX') if current_app else None) return _normalize_prefix(cfg_val) except Exception: return '' def _derive_visible_prefix(): # 1) Edge-provided header (beste en meest expliciete bron) xfp = request.headers.get('X-Forwarded-Prefix') current_app.logger.debug(f"X-Forwarded-Prefix: {xfp}") if xfp and any(str(xfp).startswith(p) for p in VISIBLE_PREFIXES): return str(xfp).rstrip('/') # 2) Referer fallback: haal het top-level segment uit de Referer path ref = request.headers.get('Referer') or '' try: ref_path = urlsplit(ref).path or '' m = re.match(r'^/(admin|api|chat-client)(?:\b|/)', ref_path) if m: return f"/{m.group(1)}" except Exception: pass # 3) Geen prefix bekend return '' def _visible_prefix_for_runtime() -> str: """Decide which prefix to use at runtime. Priority: config EVEAI_APP_PREFIX; optional dynamic fallback if enabled. """ cfg_prefix = _get_config_prefix() if cfg_prefix: current_app.logger.debug(f"prefixed_url_for: using config prefix: {cfg_prefix}") return cfg_prefix # Optional dynamic fallback use_fallback = bool(current_app.config.get('EVEAI_USE_DYNAMIC_PREFIX_FALLBACK', False)) if current_app else False if use_fallback: dyn = _derive_visible_prefix() current_app.logger.debug(f"prefixed_url_for: using dynamic fallback prefix: {dyn}") return dyn current_app.logger.debug("prefixed_url_for: no prefix configured, no fallback enabled") return '' def prefixed_url_for(endpoint, **values): """ Gedrag: - Default (_external=False, for_redirect=False): retourneer relatief pad (zonder leading '/') voor templates/JS. De dynamische zorgt voor correcte resolutie onder het zichtbare prefix. - _external=True: bouw absolute URL (schema/host). Pad wordt geprefixt met config prefix (indien gezet), of optioneel met dynamische fallback wanneer geactiveerd. - for_redirect=True: geef root-absoluut pad inclusief zichtbaar top-prefix, geschikt voor HTTP Location headers. Backwards compat: _as_location=True wordt behandeld als for_redirect. """ external = values.pop('_external', False) # Backwards compatibility met oudere paramnaam if values.pop('_as_location', False): values['for_redirect'] = True for_redirect = values.pop('for_redirect', False) generated_url = url_for(endpoint, **values) # bv. "/user/tenant_overview" path, query, fragment = urlsplit(generated_url)[2:5] if external: scheme = request.headers.get('X-Forwarded-Proto', request.scheme) host = request.headers.get('Host', request.host) visible_prefix = _visible_prefix_for_runtime() new_path = (visible_prefix.rstrip('/') + path) if (visible_prefix and not path.startswith(visible_prefix)) else path current_app.logger.debug(f"prefixed_url_for external: {scheme}://{host}{new_path}") return urlunsplit((scheme, host, new_path, query, fragment)) if for_redirect: visible_prefix = _visible_prefix_for_runtime() if visible_prefix and not path.startswith(visible_prefix): composed = f"{visible_prefix}{path}" current_app.logger.debug(f"prefixed_url_for redirect: {composed}") return composed current_app.logger.debug(f"prefixed_url_for redirect (no prefix): {path}") return path # Default: relatief pad (zonder leading '/') rel = path[1:] if path.startswith('/') else path return rel