Skip to content

Commit

Permalink
Add basic auth to FHIR server configuration (#287)
Browse files Browse the repository at this point in the history
  • Loading branch information
nickclyde authored Jan 15, 2025
1 parent f002e1a commit 7766fcf
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 63 deletions.
30 changes: 30 additions & 0 deletions query-connector/src/app/api/test-fhir-connection/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NextRequest, NextResponse } from "next/server";
import { testFhirServerConnection } from "../../query-service";

/**
* Test FHIR connection
* @param request - Incoming request
* @returns Response with the result of the test
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { url, bearerToken } = body;

if (!url) {
return NextResponse.json(
{ success: false, error: "URL is required" },
{ status: 400 },
);
}

const result = await testFhirServerConnection(url, bearerToken);
return NextResponse.json(result);
} catch (error) {
console.error("Error testing FHIR connection:", error);
return NextResponse.json(
{ success: false, error: "Internal server error" },
{ status: 500 },
);
}
}
52 changes: 49 additions & 3 deletions query-connector/src/app/database-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -766,31 +766,40 @@ export async function getFhirServerConfig(fhirServerName: string) {
* @param name - The name of the FHIR server
* @param hostname - The URL/hostname of the FHIR server
* @param lastConnectionSuccessful - Optional boolean indicating if the last connection was successful
* @param bearerToken - Optional bearer token for authentication
* @returns An object indicating success or failure with optional error message
*/
export async function insertFhirServer(
name: string,
hostname: string,
lastConnectionSuccessful?: boolean,
bearerToken?: string,
) {
const insertQuery = `
INSERT INTO fhir_servers (
name,
hostname,
last_connection_attempt,
last_connection_successful
last_connection_successful,
headers
)
VALUES ($1, $2, $3, $4);
VALUES ($1, $2, $3, $4, $5);
`;

try {
await dbClient.query("BEGIN");

// Create headers object if bearer token is provided
const headers = bearerToken
? { Authorization: `Bearer ${bearerToken}` }
: {};

const result = await dbClient.query(insertQuery, [
name,
hostname,
new Date(),
lastConnectionSuccessful,
headers,
]);

// Clear the cache so the next getFhirServerConfigs call will fetch fresh data
Expand Down Expand Up @@ -818,33 +827,70 @@ export async function insertFhirServer(
* @param name - The new name of the FHIR server
* @param hostname - The new URL/hostname of the FHIR server
* @param lastConnectionSuccessful - Optional boolean indicating if the last connection was successful
* @param bearerToken - Optional bearer token for authentication
* @returns An object indicating success or failure with optional error message
*/
export async function updateFhirServer(
id: string,
name: string,
hostname: string,
lastConnectionSuccessful?: boolean,
bearerToken?: string,
) {
const updateQuery = `
UPDATE fhir_servers
SET
name = $2,
hostname = $3,
last_connection_attempt = CURRENT_TIMESTAMP,
last_connection_successful = $4
last_connection_successful = $4,
headers = $5
WHERE id = $1
RETURNING *;
`;

try {
await dbClient.query("BEGIN");

// If updating with a bearer token, add it to existing headers
// If no bearer token provided, fetch existing headers and remove Authorization
let headers = {};
if (bearerToken) {
// Get existing headers if any
const existingServer = await dbClient.query(
"SELECT headers FROM fhir_servers WHERE id = $1",
[id],
);
if (existingServer.rows.length > 0) {
// Keep existing headers and add/update Authorization
headers = {
...existingServer.rows[0].headers,
Authorization: `Bearer ${bearerToken}`,
};
} else {
// No existing headers, just set Authorization
headers = { Authorization: `Bearer ${bearerToken}` };
}
} else {
// Get existing headers if any and remove Authorization
const existingServer = await dbClient.query(
"SELECT headers FROM fhir_servers WHERE id = $1",
[id],
);
if (existingServer.rows.length > 0) {
const existingHeaders = existingServer.rows[0].headers || {};
// Remove Authorization if it exists when switching to no auth
const { Authorization, ...restHeaders } = existingHeaders;
headers = restHeaders;
}
}

const result = await dbClient.query(updateQuery, [
id,
name,
hostname,
lastConnectionSuccessful,
headers,
]);

// Clear the cache so the next getFhirServerConfigs call will fetch fresh data
Expand Down
113 changes: 54 additions & 59 deletions query-connector/src/app/fhir-servers/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ const FhirServers: React.FC = () => {
const [fhirServers, setFhirServers] = useState<FhirServerConfig[]>([]);
const [serverName, setServerName] = useState("");
const [serverUrl, setServerUrl] = useState("");
const [authMethod, setAuthMethod] = useState<"none" | "basic">("none");
const [bearerToken, setBearerToken] = useState("");
const [connectionStatus, setConnectionStatus] = useState<
"idle" | "success" | "error"
>("idle");
Expand Down Expand Up @@ -62,6 +64,8 @@ const FhirServers: React.FC = () => {
const resetModalState = () => {
setServerName("");
setServerUrl("");
setAuthMethod("none");
setBearerToken("");
setConnectionStatus("idle");
setErrorMessage("");
setSelectedServer(null);
Expand All @@ -74,6 +78,15 @@ const FhirServers: React.FC = () => {
setServerName(server.name);
setServerUrl(server.hostname);
setConnectionStatus("idle");

// Set auth method and bearer token if they exist
if (server.headers?.Authorization?.startsWith("Bearer ")) {
setAuthMethod("basic");
setBearerToken(server.headers.Authorization.replace("Bearer ", ""));
} else {
setAuthMethod("none");
setBearerToken("");
}
} else {
resetModalState();
}
Expand All @@ -97,72 +110,23 @@ const FhirServers: React.FC = () => {
return { success: false, error: "Client-side only operation" };

try {
const baseUrl = url.replace(/\/$/, "");
const metadataUrl = `${baseUrl}/metadata`;

const response = await fetch(metadataUrl, {
method: "GET",
const response = await fetch("/api/test-fhir-connection", {
method: "POST",
headers: {
Accept: "application/fhir+json",
"Content-Type": "application/json",
},
body: JSON.stringify({
url,
bearerToken: authMethod === "basic" ? bearerToken : undefined,
}),
});

if (response.ok) {
const data = await response.json();
if (data.resourceType === "CapabilityStatement") {
return { success: true };
} else {
return {
success: false,
error:
"Invalid FHIR server response: Server did not return a valid CapabilityStatement",
};
}
} else {
let errorMessage: string;
switch (response.status) {
case 401:
errorMessage =
"Connection failed: Authentication required. Please check your credentials.";
break;
case 403:
errorMessage =
"Connection failed: Access forbidden. You do not have permission to access this FHIR server.";
break;
case 404:
errorMessage =
"Connection failed: The FHIR server endpoint was not found. Please verify the URL.";
break;
case 408:
errorMessage =
"Connection failed: The request timed out. The FHIR server took too long to respond.";
break;
case 500:
errorMessage =
"Connection failed: Internal server error. The FHIR server encountered an unexpected condition.";
break;
case 502:
errorMessage =
"Connection failed: Bad gateway. The FHIR server received an invalid response from upstream.";
break;
case 503:
errorMessage =
"Connection failed: The FHIR server is temporarily unavailable or under maintenance.";
break;
case 504:
errorMessage =
"Connection failed: Gateway timeout. The upstream server did not respond in time.";
break;
default:
errorMessage = `Connection failed: The FHIR server returned an error. (${response.status} ${response.statusText})`;
}
return { success: false, error: errorMessage };
}
const result = await response.json();
return result;
} catch (error) {
return {
success: false,
error:
"Connection failed: Unable to reach the FHIR server. Please check if the URL is correct and the server is accessible.",
error: "Failed to test connection. Please try again.",
};
}
};
Expand All @@ -183,6 +147,7 @@ const FhirServers: React.FC = () => {
serverName,
serverUrl,
connectionResult.success,
authMethod === "basic" ? bearerToken : undefined,
);

if (result.success) {
Expand All @@ -200,6 +165,7 @@ const FhirServers: React.FC = () => {
serverName,
serverUrl,
connectionResult.success,
authMethod === "basic" ? bearerToken : undefined,
);

if (result.success) {
Expand Down Expand Up @@ -409,6 +375,35 @@ const FhirServers: React.FC = () => {
onChange={(e) => setServerUrl(e.target.value)}
required
/>
<Label htmlFor="auth-method">Auth Method</Label>
<select
className="usa-select"
id="auth-method"
name="auth-method"
value={authMethod}
onChange={(e) => {
setAuthMethod(e.target.value as "none" | "basic");
if (e.target.value === "none") {
setBearerToken("");
}
}}
>
<option value="none">None</option>
<option value="basic">Basic auth</option>
</select>
{authMethod === "basic" && (
<>
<Label htmlFor="bearer-token">Bearer Token</Label>
<TextInput
id="bearer-token"
name="bearer-token"
type="text"
value={bearerToken}
onChange={(e) => setBearerToken(e.target.value)}
required
/>
</>
)}
</Modal>
</div>
</>
Expand Down
Loading

0 comments on commit 7766fcf

Please sign in to comment.