Compare commits

..

3 Commits

Author SHA1 Message Date
f4c457043a hotfix 2025-08-27 19:21:53 +10:00
dffe43beee rollback 2025-08-27 16:42:52 +10:00
3f1ac10290 hotfix 2025-08-27 16:19:52 +10:00
4 changed files with 407 additions and 507 deletions

75
app.py
View File

@ -536,6 +536,29 @@ def restart_tomcat():
def config_hapi(): def config_hapi():
return render_template('config_hapi.html', site_name='FHIRFLARE IG Toolkit', now=datetime.datetime.now()) return render_template('config_hapi.html', site_name='FHIRFLARE IG Toolkit', now=datetime.datetime.now())
@app.route('/api/get-local-server-url', methods=['GET'])
@swag_from({
'tags': ['FHIR Server Configuration'],
'summary': 'Get the local FHIR server URL.',
'description': 'Retrieves the base URL of the configured local FHIR server (HAPI). This is used by frontend components to make direct requests, bypassing the proxy.',
'responses': {
'200': {
'description': 'The URL of the local FHIR server.',
'schema': {
'type': 'object',
'properties': {
'url': {'type': 'string', 'example': 'http://localhost:8080/fhir'}
}
}
}
}
})
def get_local_server_url():
"""
Expose the local HAPI FHIR server URL to the frontend.
"""
return jsonify({'url': app.config.get('HAPI_FHIR_URL', 'http://localhost:8080/fhir')})
@app.route('/manual-import-ig', methods=['GET', 'POST']) @app.route('/manual-import-ig', methods=['GET', 'POST'])
def manual_import_ig(): def manual_import_ig():
""" """
@ -1780,40 +1803,58 @@ def fhir_ui_operations():
# Use a single route to capture everything after /fhir/ # Use a single route to capture everything after /fhir/
# The 'path' converter handles slashes. 'subpath' can be empty. # The 'path' converter handles slashes. 'subpath' can be empty.
@app.route('/fhir', defaults={'subpath': ''}, methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH']) @app.route('/fhir', defaults={'subpath': ''}, methods=['GET', 'POST', 'PUT', 'DELETE'])
@app.route('/fhir/', defaults={'subpath': ''}, methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH']) @app.route('/fhir/', defaults={'subpath': ''}, methods=['GET', 'POST', 'PUT', 'DELETE'])
@app.route('/fhir/<path:subpath>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH']) @app.route('/fhir/<path:subpath>', methods=['GET', 'POST', 'PUT', 'DELETE'])
def proxy_hapi(subpath): def proxy_hapi(subpath):
""" """
Proxies FHIR requests to either the local HAPI server or a custom Proxies FHIR requests to either the local HAPI server or a custom
target server specified by the 'X-Target-FHIR-Server' header or a query parameter. target server specified by the 'X-Target-FHIR-Server' header.
Handles requests to /fhir/ (base, subpath='') and /fhir/<subpath>.
The route '/fhir' (no trailing slash) is handled separately for the UI.
""" """
logger.debug(f"Proxy received request for path: '/fhir/{subpath}'") # Clean subpath just in case prefixes were somehow included
clean_subpath = subpath.replace('r4/', '', 1).replace('fhir/', '', 1).strip('/')
logger.debug(f"Proxy received request for path: '/fhir/{subpath}', cleaned subpath: '{clean_subpath}'")
# Determine the target FHIR server base URL # Determine the target FHIR server base URL
# NEW: Check for proxy-target query parameter first
target_server_query = request.args.get('proxy-target') target_server_query = request.args.get('proxy-target')
target_server_header = request.headers.get('X-Target-FHIR-Server') target_server_header = request.headers.get('X-Target-FHIR-Server')
final_base_url = None final_base_url = None
is_custom_target = False is_custom_target = False
if target_server_query: if target_server_query:
final_base_url = target_server_query.rstrip('/') try:
is_custom_target = True parsed_url = urlparse(target_server_query)
logger.info(f"Proxy target identified from query parameter: {final_base_url}") if not parsed_url.scheme or not parsed_url.netloc:
raise ValueError("Invalid URL format in proxy-target query parameter")
final_base_url = target_server_query.rstrip('/')
is_custom_target = True
logger.info(f"Proxy target identified from query parameter: {final_base_url}")
except ValueError as e:
logger.warning(f"Invalid URL in proxy-target query parameter: '{target_server_query}'. Falling back. Error: {e}")
final_base_url = current_app.config['HAPI_FHIR_URL'].rstrip('/')
logger.debug(f"Falling back to default local HAPI due to invalid query: {final_base_url}")
elif target_server_header: elif target_server_header:
final_base_url = target_server_header.rstrip('/') try:
is_custom_target = True parsed_url = urlparse(target_server_header)
logger.info(f"Proxy target identified from header: {final_base_url}") if not parsed_url.scheme or not parsed_url.netloc:
raise ValueError("Invalid URL format in X-Target-FHIR-Server header")
final_base_url = target_server_header.rstrip('/')
is_custom_target = True
logger.info(f"Proxy target identified from header: {final_base_url}")
except ValueError as e:
logger.warning(f"Invalid URL in X-Target-FHIR-Server header: '{target_server_header}'. Falling back. Error: {e}")
final_base_url = current_app.config['HAPI_FHIR_URL'].rstrip('/')
logger.debug(f"Falling back to default local HAPI due to invalid header: {final_base_url}")
else: else:
final_base_url = current_app.config['HAPI_FHIR_URL'].rstrip('/') final_base_url = current_app.config['HAPI_FHIR_URL'].rstrip('/')
logger.debug(f"No target info found, proxying to default local HAPI: {final_base_url}") logger.debug(f"No target info found, proxying to default local HAPI: {final_base_url}")
# Construct the final URL for the target server request. This is the core logical change. # Construct the final URL for the target server request
# The subpath variable now correctly captures the remaining part of the URL after '/fhir/'. # Append the cleaned subpath only if it's not empty
# We construct the final URL by joining the base URL and the subpath. final_url = f"{final_base_url}/{clean_subpath}" if clean_subpath else final_base_url
final_url = f"{final_base_url}/{subpath}" if subpath else final_base_url
logger.debug(f"Final URL for target server: {final_url}")
# Prepare headers to forward # Prepare headers to forward
headers_to_forward = { headers_to_forward = {

View File

@ -141,6 +141,7 @@ document.addEventListener('DOMContentLoaded', function() {
// --- State Variable --- // --- State Variable ---
let useLocalHapi = true; let useLocalHapi = true;
let localHapiBaseUrl = ''; // New variable to store the fetched URL
// --- Get App Mode from Flask Context --- // --- Get App Mode from Flask Context ---
const appMode = '{{ app_mode | default("standalone") | lower }}'; const appMode = '{{ app_mode | default("standalone") | lower }}';
@ -193,6 +194,22 @@ document.addEventListener('DOMContentLoaded', function() {
} catch (err) { console.error('Copy failed:', err); } } catch (err) { console.error('Copy failed:', err); }
} }
async function fetchLocalUrlAndSetUI() {
try {
const response = await fetch('/api/get-local-server-url');
if (!response.ok) throw new Error('Failed to fetch local server URL.');
const data = await response.json();
localHapiBaseUrl = data.url;
console.log(`Local HAPI URL fetched: ${localHapiBaseUrl}`);
} catch (error) {
console.error(error);
localHapiBaseUrl = '/fhir'; // Fallback to proxy
alert('Could not fetch local HAPI server URL. Falling back to proxy.');
} finally {
updateServerToggleUI();
}
}
function updateServerToggleUI() { function updateServerToggleUI() {
if (appMode === 'lite') { if (appMode === 'lite') {
useLocalHapi = false; useLocalHapi = false;
@ -260,7 +277,7 @@ document.addEventListener('DOMContentLoaded', function() {
} }
// --- Initial Setup --- // --- Initial Setup ---
updateServerToggleUI(); fetchLocalUrlAndSetUI(); // Fetch the URL and then set up the UI
updateRequestBodyVisibility(); updateRequestBodyVisibility();
// --- Event Listeners --- // --- Event Listeners ---
@ -312,14 +329,30 @@ document.addEventListener('DOMContentLoaded', function() {
sendButton.textContent = 'Send Request'; sendButton.textContent = 'Send Request';
return; return;
} }
if (!useLocalHapi && !customUrl) {
alert('Please enter a custom FHIR Server URL.'); // --- Determine Final URL & Headers ---
fhirServerUrlInput.classList.add('is-invalid'); const cleanedPath = cleanFhirPath(path);
sendButton.disabled = false; const headers = { 'Accept': 'application/fhir+json, application/fhir+xml;q=0.9, */*;q=0.8' };
sendButton.textContent = 'Send Request'; let finalFetchUrl;
return;
} if (useLocalHapi) {
if (!useLocalHapi && customUrl) { if (!localHapiBaseUrl) {
alert('Local HAPI URL not available. Please refresh the page.');
sendButton.disabled = false;
sendButton.textContent = 'Send Request';
return;
}
// Direct request to the local URL
finalFetchUrl = `${localHapiBaseUrl.replace(/\/+$/, '')}/${cleanedPath}`;
} else {
// Use the proxy for custom URLs
if (!customUrl) {
alert('Please enter a custom FHIR Server URL.');
fhirServerUrlInput.classList.add('is-invalid');
sendButton.disabled = false;
sendButton.textContent = 'Send Request';
return;
}
try { new URL(customUrl); } try { new URL(customUrl); }
catch (_) { catch (_) {
alert('Invalid custom FHIR Server URL format.'); alert('Invalid custom FHIR Server URL format.');
@ -328,24 +361,16 @@ document.addEventListener('DOMContentLoaded', function() {
sendButton.textContent = 'Send Request'; sendButton.textContent = 'Send Request';
return; return;
} }
} finalFetchUrl = '/fhir/' + cleanedPath;
headers['X-Target-FHIR-Server'] = customUrl.replace(/\/+$/, '');
// --- Validate Authentication --- console.log("Adding header X-Target-FHIR-Server:", headers['X-Target-FHIR-Server']);
if (!useLocalHapi) { if (authType === 'bearer') {
if (authType === 'bearer' && !bearerToken) { headers['Authorization'] = `Bearer ${bearerToken}`;
alert('Please enter a Bearer Token.'); console.log("Adding header Authorization: Bearer <truncated>");
bearerTokenInput.classList.add('is-invalid'); } else if (authType === 'basic') {
sendButton.disabled = false; const credentials = btoa(`${username}:${password}`);
sendButton.textContent = 'Send Request'; headers['Authorization'] = `Basic ${credentials}`;
return; console.log("Adding header Authorization: Basic <redacted>");
}
if (authType === 'basic' && (!username || !password)) {
alert('Please enter both Username and Password for Basic Authentication.');
if (!username) usernameInput.classList.add('is-invalid');
if (!password) passwordInput.classList.add('is-invalid');
sendButton.disabled = false;
sendButton.textContent = 'Send Request';
return;
} }
} }
@ -361,31 +386,6 @@ document.addEventListener('DOMContentLoaded', function() {
if (body === '') body = ''; if (body === '') body = '';
} }
// --- Determine Fetch URL and Headers ---
const cleanedPath = cleanFhirPath(path);
const finalFetchUrl = '/fhir/' + cleanedPath;
const headers = { 'Accept': 'application/fhir+json, application/fhir+xml;q=0.9, */*;q=0.8' };
if (body !== undefined) {
if (body.trim().startsWith('{')) { headers['Content-Type'] = 'application/fhir+json'; }
else if (body.trim().startsWith('<')) { headers['Content-Type'] = 'application/fhir+xml'; }
else if (method === 'POST' && path.endsWith('_search') && body && !body.trim().startsWith('{') && !body.trim().startsWith('<')) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; }
else if (body) { headers['Content-Type'] = 'application/fhir+json'; }
}
if (!useLocalHapi && customUrl) {
headers['X-Target-FHIR-Server'] = customUrl.replace(/\/+$/, '');
console.log("Adding header X-Target-FHIR-Server:", headers['X-Target-FHIR-Server']);
if (authType === 'bearer') {
headers['Authorization'] = `Bearer ${bearerToken}`;
console.log("Adding header Authorization: Bearer <truncated>");
} else if (authType === 'basic') {
const credentials = btoa(`${username}:${password}`);
headers['Authorization'] = `Basic ${credentials}`;
console.log("Adding header Authorization: Basic <redacted>");
}
}
const csrfTokenInput = form.querySelector('input[name="csrf_token"]'); const csrfTokenInput = form.querySelector('input[name="csrf_token"]');
const csrfToken = csrfTokenInput ? csrfTokenInput.value : null; const csrfToken = csrfTokenInput ? csrfTokenInput.value : null;
if (useLocalHapi && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method) && csrfToken) { if (useLocalHapi && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method) && csrfToken) {

File diff suppressed because it is too large Load Diff

View File

@ -181,6 +181,7 @@ document.addEventListener('DOMContentLoaded', () => {
let retrieveZipPath = null; let retrieveZipPath = null;
let splitZipPath = null; let splitZipPath = null;
let fetchedMetadataCache = null; let fetchedMetadataCache = null;
let localHapiBaseUrl = ''; // NEW: To store the fetched URL
// --- Helper Functions --- // --- Helper Functions ---
const sanitizeText = (str) => str ? String(str).replace(/</g, "&lt;").replace(/>/g, "&gt;") : ""; const sanitizeText = (str) => str ? String(str).replace(/</g, "&lt;").replace(/>/g, "&gt;") : "";
@ -196,6 +197,22 @@ document.addEventListener('DOMContentLoaded', () => {
} }
} }
async function fetchLocalUrlAndSetUI() {
try {
const response = await fetch('/api/get-local-server-url');
if (!response.ok) throw new Error('Failed to fetch local server URL.');
const data = await response.json();
localHapiBaseUrl = data.url;
console.log(`Local HAPI URL fetched: ${localHapiBaseUrl}`);
} catch (error) {
console.error(error);
localHapiBaseUrl = '/fhir'; // Fallback to proxy
alert('Could not fetch local HAPI server URL. Falling back to proxy.');
} finally {
updateServerToggleUI();
}
}
function updateServerToggleUI() { function updateServerToggleUI() {
if (!toggleLabel || !fhirServerUrlInput || !toggleServerButton || !authSection) return; if (!toggleLabel || !fhirServerUrlInput || !toggleServerButton || !authSection) return;
@ -277,15 +294,16 @@ document.addEventListener('DOMContentLoaded', () => {
fetchMetadataButton.addEventListener('click', async () => { fetchMetadataButton.addEventListener('click', async () => {
resourceButtonsContainer.innerHTML = '<span class="text-muted">Fetching...</span>'; resourceButtonsContainer.innerHTML = '<span class="text-muted">Fetching...</span>';
resourceTypesDiv.style.display = 'block'; resourceTypesDiv.style.display = 'block';
const customUrl = useLocalHapi ? null : fhirServerUrlInput.value.trim().replace(/\/+$/, '');
if (!useLocalHapi && !customUrl) { let customUrl = null;
fhirServerUrlInput.classList.add('is-invalid');
alert('Please enter a valid FHIR server URL.');
resourceButtonsContainer.innerHTML = '<span class="text-danger">Error: Custom URL required.</span>';
return;
}
if (!useLocalHapi) { if (!useLocalHapi) {
customUrl = fhirServerUrlInput.value.trim().replace(/\/+$/, '');
if (!customUrl) {
fhirServerUrlInput.classList.add('is-invalid');
alert('Please enter a valid FHIR server URL.');
resourceButtonsContainer.innerHTML = '<span class="text-danger">Error: Custom URL required.</span>';
return;
}
try { new URL(customUrl); } catch (_) { try { new URL(customUrl); } catch (_) {
fhirServerUrlInput.classList.add('is-invalid'); fhirServerUrlInput.classList.add('is-invalid');
alert('Invalid custom URL format.'); alert('Invalid custom URL format.');
@ -299,23 +317,29 @@ document.addEventListener('DOMContentLoaded', () => {
fetchMetadataButton.textContent = 'Fetching...'; fetchMetadataButton.textContent = 'Fetching...';
try { try {
const fetchUrl = useLocalHapi ? '/fhir/metadata' : `${customUrl}/metadata`; // REVISED LOGIC: Build URL and headers based on the toggle state
let fetchUrl;
const headers = { 'Accept': 'application/fhir+json' }; const headers = { 'Accept': 'application/fhir+json' };
if (!useLocalHapi && customUrl) {
if (authTypeSelect && authTypeSelect.value !== 'none') { if (useLocalHapi) {
const authType = authTypeSelect.value; if (!localHapiBaseUrl) {
if (authType === 'bearer' && bearerTokenInput && bearerTokenInput.value) { throw new Error("Local HAPI URL not available. Please refresh.");
headers['Authorization'] = `Bearer ${bearerTokenInput.value}`;
} else if (authType === 'basic' && usernameInput && passwordInput && usernameInput.value && passwordInput.value) {
const credentials = btoa(`${usernameInput.value}:${passwordInput.value}`);
headers['Authorization'] = `Basic ${credentials}`;
}
} }
console.log(`Fetching metadata directly from: ${customUrl}`); fetchUrl = `${localHapiBaseUrl}/metadata`;
console.log(`Fetching metadata directly from local URL: ${fetchUrl}`);
} else { } else {
console.log("Fetching metadata from local HAPI server via proxy"); fetchUrl = `${customUrl}/metadata`;
// Add authentication headers for the direct request to the custom server
const authType = authTypeSelect.value;
if (authType === 'bearer' && bearerTokenInput && bearerTokenInput.value) {
headers['Authorization'] = `Bearer ${bearerTokenInput.value}`;
} else if (authType === 'basic' && usernameInput && passwordInput && usernameInput.value && passwordInput.value) {
const credentials = btoa(`${usernameInput.value}:${passwordInput.value}`);
headers['Authorization'] = `Basic ${credentials}`;
}
console.log(`Fetching metadata directly from custom URL: ${fetchUrl}`);
} }
console.log(`Fetch URL: ${fetchUrl}`);
console.log(`Request Headers sent: ${JSON.stringify(headers)}`); console.log(`Request Headers sent: ${JSON.stringify(headers)}`);
const response = await fetch(fetchUrl, { method: 'GET', headers: headers }); const response = await fetch(fetchUrl, { method: 'GET', headers: headers });
@ -381,9 +405,9 @@ document.addEventListener('DOMContentLoaded', () => {
return; return;
} }
const currentFhirServerUrl = useLocalHapi ? null : fhirServerUrlInput.value.trim(); const currentFhirServerUrl = useLocalHapi ? localHapiBaseUrl : fhirServerUrlInput.value.trim();
if (!useLocalHapi && !currentFhirServerUrl) { if (!currentFhirServerUrl) {
alert('Custom FHIR Server URL is required.'); alert('FHIR Server URL is required.');
fhirServerUrlInput.classList.add('is-invalid'); fhirServerUrlInput.classList.add('is-invalid');
retrieveButton.disabled = false; retrieveButton.disabled = false;
if (spinner) spinner.style.display = 'none'; if (spinner) spinner.style.display = 'none';
@ -396,14 +420,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (csrfTokenInput) formData.append('csrf_token', csrfTokenInput.value); if (csrfTokenInput) formData.append('csrf_token', csrfTokenInput.value);
selectedResources.forEach(res => formData.append('resources', res)); selectedResources.forEach(res => formData.append('resources', res));
// --- FIX APPLIED HERE --- formData.append('fhir_server_url', currentFhirServerUrl);
if (!useLocalHapi && currentFhirServerUrl) {
formData.append('fhir_server_url', currentFhirServerUrl);
} else {
// Explicitly use the proxy URL if local HAPI is selected
formData.append('fhir_server_url', '/fhir');
}
// --- END FIX ---
if (validateReferencesCheckbox) { if (validateReferencesCheckbox) {
formData.append('validate_references', validateReferencesCheckbox.checked ? 'true' : 'false'); formData.append('validate_references', validateReferencesCheckbox.checked ? 'true' : 'false');
@ -688,7 +705,7 @@ document.addEventListener('DOMContentLoaded', () => {
// --- Initial Setup Calls --- // --- Initial Setup Calls ---
updateBundleSourceUI(); updateBundleSourceUI();
updateServerToggleUI(); fetchLocalUrlAndSetUI();
toggleFetchReferenceBundles(); toggleFetchReferenceBundles();
}); });
</script> </script>