mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-09-17 14:25:02 +00:00
Compare commits
3 Commits
85f980be0a
...
f4c457043a
Author | SHA1 | Date | |
---|---|---|---|
f4c457043a | |||
dffe43beee | |||
3f1ac10290 |
77
app.py
77
app.py
@ -536,6 +536,29 @@ def restart_tomcat():
|
||||
def config_hapi():
|
||||
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'])
|
||||
def manual_import_ig():
|
||||
"""
|
||||
@ -1780,41 +1803,59 @@ def fhir_ui_operations():
|
||||
|
||||
# Use a single route to capture everything after /fhir/
|
||||
# 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', 'PATCH'])
|
||||
@app.route('/fhir/<path: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'])
|
||||
@app.route('/fhir/<path:subpath>', methods=['GET', 'POST', 'PUT', 'DELETE'])
|
||||
def proxy_hapi(subpath):
|
||||
"""
|
||||
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
|
||||
# NEW: Check for proxy-target query parameter first
|
||||
target_server_query = request.args.get('proxy-target')
|
||||
target_server_header = request.headers.get('X-Target-FHIR-Server')
|
||||
final_base_url = None
|
||||
is_custom_target = False
|
||||
|
||||
if target_server_query:
|
||||
final_base_url = target_server_query.rstrip('/')
|
||||
is_custom_target = True
|
||||
logger.info(f"Proxy target identified from query parameter: {final_base_url}")
|
||||
try:
|
||||
parsed_url = urlparse(target_server_query)
|
||||
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:
|
||||
final_base_url = target_server_header.rstrip('/')
|
||||
is_custom_target = True
|
||||
logger.info(f"Proxy target identified from header: {final_base_url}")
|
||||
try:
|
||||
parsed_url = urlparse(target_server_header)
|
||||
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:
|
||||
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}")
|
||||
|
||||
# Construct the final URL for the target server request. This is the core logical change.
|
||||
# The subpath variable now correctly captures the remaining part of the URL after '/fhir/'.
|
||||
# We construct the final URL by joining the base URL and the subpath.
|
||||
final_url = f"{final_base_url}/{subpath}" if subpath else final_base_url
|
||||
|
||||
logger.debug(f"Final URL for target server: {final_url}")
|
||||
|
||||
# Construct the final URL for the target server request
|
||||
# Append the cleaned subpath only if it's not empty
|
||||
final_url = f"{final_base_url}/{clean_subpath}" if clean_subpath else final_base_url
|
||||
|
||||
# Prepare headers to forward
|
||||
headers_to_forward = {
|
||||
k: v for k, v in request.headers.items()
|
||||
|
@ -141,6 +141,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// --- State Variable ---
|
||||
let useLocalHapi = true;
|
||||
let localHapiBaseUrl = ''; // New variable to store the fetched URL
|
||||
|
||||
// --- Get App Mode from Flask Context ---
|
||||
const appMode = '{{ app_mode | default("standalone") | lower }}';
|
||||
@ -193,6 +194,22 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
} 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() {
|
||||
if (appMode === 'lite') {
|
||||
useLocalHapi = false;
|
||||
@ -260,7 +277,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
|
||||
// --- Initial Setup ---
|
||||
updateServerToggleUI();
|
||||
fetchLocalUrlAndSetUI(); // Fetch the URL and then set up the UI
|
||||
updateRequestBodyVisibility();
|
||||
|
||||
// --- Event Listeners ---
|
||||
@ -312,14 +329,30 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
sendButton.textContent = 'Send Request';
|
||||
return;
|
||||
}
|
||||
if (!useLocalHapi && !customUrl) {
|
||||
alert('Please enter a custom FHIR Server URL.');
|
||||
fhirServerUrlInput.classList.add('is-invalid');
|
||||
sendButton.disabled = false;
|
||||
sendButton.textContent = 'Send Request';
|
||||
return;
|
||||
}
|
||||
if (!useLocalHapi && customUrl) {
|
||||
|
||||
// --- Determine Final URL & Headers ---
|
||||
const cleanedPath = cleanFhirPath(path);
|
||||
const headers = { 'Accept': 'application/fhir+json, application/fhir+xml;q=0.9, */*;q=0.8' };
|
||||
let finalFetchUrl;
|
||||
|
||||
if (useLocalHapi) {
|
||||
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); }
|
||||
catch (_) {
|
||||
alert('Invalid custom FHIR Server URL format.');
|
||||
@ -328,52 +361,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
sendButton.textContent = 'Send Request';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Validate Authentication ---
|
||||
if (!useLocalHapi) {
|
||||
if (authType === 'bearer' && !bearerToken) {
|
||||
alert('Please enter a Bearer Token.');
|
||||
bearerTokenInput.classList.add('is-invalid');
|
||||
sendButton.disabled = false;
|
||||
sendButton.textContent = 'Send Request';
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Validate & Get Body ---
|
||||
if (method === 'POST' || method === 'PUT') {
|
||||
body = validateRequestBody(method, path);
|
||||
if (body === null) {
|
||||
alert('Request body contains invalid JSON/Format.');
|
||||
sendButton.disabled = false;
|
||||
sendButton.textContent = 'Send Request';
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
finalFetchUrl = '/fhir/' + cleanedPath;
|
||||
headers['X-Target-FHIR-Server'] = customUrl.replace(/\/+$/, '');
|
||||
console.log("Adding header X-Target-FHIR-Server:", headers['X-Target-FHIR-Server']);
|
||||
if (authType === 'bearer') {
|
||||
@ -385,6 +373,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log("Adding header Authorization: Basic <redacted>");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Validate & Get Body ---
|
||||
if (method === 'POST' || method === 'PUT') {
|
||||
body = validateRequestBody(method, path);
|
||||
if (body === null) {
|
||||
alert('Request body contains invalid JSON/Format.');
|
||||
sendButton.disabled = false;
|
||||
sendButton.textContent = 'Send Request';
|
||||
return;
|
||||
}
|
||||
if (body === '') body = '';
|
||||
}
|
||||
|
||||
const csrfTokenInput = form.querySelector('input[name="csrf_token"]');
|
||||
const csrfToken = csrfTokenInput ? csrfTokenInput.value : null;
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -181,6 +181,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
let retrieveZipPath = null;
|
||||
let splitZipPath = null;
|
||||
let fetchedMetadataCache = null;
|
||||
let localHapiBaseUrl = ''; // NEW: To store the fetched URL
|
||||
|
||||
// --- Helper Functions ---
|
||||
const sanitizeText = (str) => str ? String(str).replace(/</g, "<").replace(/>/g, ">") : "";
|
||||
@ -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() {
|
||||
if (!toggleLabel || !fhirServerUrlInput || !toggleServerButton || !authSection) return;
|
||||
|
||||
@ -277,15 +294,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
fetchMetadataButton.addEventListener('click', async () => {
|
||||
resourceButtonsContainer.innerHTML = '<span class="text-muted">Fetching...</span>';
|
||||
resourceTypesDiv.style.display = 'block';
|
||||
const customUrl = useLocalHapi ? null : fhirServerUrlInput.value.trim().replace(/\/+$/, '');
|
||||
|
||||
if (!useLocalHapi && !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;
|
||||
}
|
||||
|
||||
let customUrl = null;
|
||||
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 (_) {
|
||||
fhirServerUrlInput.classList.add('is-invalid');
|
||||
alert('Invalid custom URL format.');
|
||||
@ -299,23 +317,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
fetchMetadataButton.textContent = 'Fetching...';
|
||||
|
||||
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' };
|
||||
if (!useLocalHapi && customUrl) {
|
||||
if (authTypeSelect && authTypeSelect.value !== 'none') {
|
||||
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}`;
|
||||
}
|
||||
|
||||
if (useLocalHapi) {
|
||||
if (!localHapiBaseUrl) {
|
||||
throw new Error("Local HAPI URL not available. Please refresh.");
|
||||
}
|
||||
console.log(`Fetching metadata directly from: ${customUrl}`);
|
||||
fetchUrl = `${localHapiBaseUrl}/metadata`;
|
||||
console.log(`Fetching metadata directly from local URL: ${fetchUrl}`);
|
||||
} 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)}`);
|
||||
|
||||
const response = await fetch(fetchUrl, { method: 'GET', headers: headers });
|
||||
@ -381,9 +405,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentFhirServerUrl = useLocalHapi ? null : fhirServerUrlInput.value.trim();
|
||||
if (!useLocalHapi && !currentFhirServerUrl) {
|
||||
alert('Custom FHIR Server URL is required.');
|
||||
const currentFhirServerUrl = useLocalHapi ? localHapiBaseUrl : fhirServerUrlInput.value.trim();
|
||||
if (!currentFhirServerUrl) {
|
||||
alert('FHIR Server URL is required.');
|
||||
fhirServerUrlInput.classList.add('is-invalid');
|
||||
retrieveButton.disabled = false;
|
||||
if (spinner) spinner.style.display = 'none';
|
||||
@ -396,14 +420,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (csrfTokenInput) formData.append('csrf_token', csrfTokenInput.value);
|
||||
selectedResources.forEach(res => formData.append('resources', res));
|
||||
|
||||
// --- FIX APPLIED HERE ---
|
||||
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 ---
|
||||
formData.append('fhir_server_url', currentFhirServerUrl);
|
||||
|
||||
if (validateReferencesCheckbox) {
|
||||
formData.append('validate_references', validateReferencesCheckbox.checked ? 'true' : 'false');
|
||||
@ -688,7 +705,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// --- Initial Setup Calls ---
|
||||
updateBundleSourceUI();
|
||||
updateServerToggleUI();
|
||||
fetchLocalUrlAndSetUI();
|
||||
toggleFetchReferenceBundles();
|
||||
});
|
||||
</script>
|
||||
|
Loading…
x
Reference in New Issue
Block a user