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

77
app.py
View File

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

View File

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

View File

@ -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, "&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() {
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>