mirror of
https://github.com/AmanTahiliani/FHIR-Sandbox.git
synced 2026-08-07 11:53:56 -04:00
Initial Commit
This commit is contained in:
370
app/main.go
Normal file
370
app/main.go
Normal file
@@ -0,0 +1,370 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Application struct {
|
||||
config *ApplicationConfig
|
||||
}
|
||||
|
||||
type ApplicationConfig struct {
|
||||
smart *SMARTAppConfig
|
||||
eHRs []EHRClientsConfig // List of EHR clients configuration
|
||||
}
|
||||
|
||||
type SMARTAppConfig struct {
|
||||
oAuth1 bool // Enable OAuth1 for certain EHRs
|
||||
oAuth2 bool // Enable OAuth2 for others
|
||||
redirectPath string // Where to redirect after auth
|
||||
clientID string // OAuth client ID (if applicable)
|
||||
clientSecret string // OAuth client secret
|
||||
scopes []string
|
||||
}
|
||||
|
||||
type EHRClientsConfig struct {
|
||||
name string // Name of the EHR, e.g., "EHR_A"
|
||||
url string // FHIR endpoint URL
|
||||
authType string // auth-1 or auth-2 for OAuth type
|
||||
clientID string // OAuth client ID (for Azure AD)
|
||||
clientSecret string // OAuth secret (for Azure AD)
|
||||
patientAPI bool // Whether to enable patient API endpoints
|
||||
}
|
||||
|
||||
type LaunchContext struct {
|
||||
LaunchID string `json:"launch"`
|
||||
Patient string `json:"patient"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.Printf("Starting EHR integration application...")
|
||||
|
||||
appConfig := &ApplicationConfig{
|
||||
smart: &SMARTAppConfig{
|
||||
redirectPath: "http://localhost:8080/auth-redirect",
|
||||
clientID: "my-smart-client-id",
|
||||
clientSecret: "my-smart-client-secret",
|
||||
scopes: []string{"launch", "patient/*.read"},
|
||||
oAuth1: true,
|
||||
oAuth2: false,
|
||||
},
|
||||
eHRs: []EHRClientsConfig{ // Sample EHR clients configuration
|
||||
{
|
||||
name: "EHR_A",
|
||||
url: "https://launch.smarthealthit.org/v/r4/fhir",
|
||||
authType: "auth-2",
|
||||
clientID: "abcdefghijklmnopqrst",
|
||||
clientSecret: "ehr_a_client_secret",
|
||||
patientAPI: true,
|
||||
},
|
||||
{
|
||||
name: "EHR_B",
|
||||
url: "https://ehr-b.healthcare-provider.com/fhir/v2",
|
||||
authType: "auth-2",
|
||||
clientID: "ehr_b_client_id", // OAuth 2.0 client ID
|
||||
clientSecret: "ehr_b_client_secret",
|
||||
patientAPI: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app := &Application{
|
||||
config: appConfig,
|
||||
}
|
||||
|
||||
log.Printf("Listening on port %d...", 8080)
|
||||
|
||||
// Start HTTP server and handle routes
|
||||
http.HandleFunc("/", app.handleRoot)
|
||||
http.HandleFunc("/launch", app.handleLaunch)
|
||||
http.HandleFunc("/auth-redirect", app.handleAuthRedirect)
|
||||
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
||||
// handleRoot is a method of Application that handles the root endpoint.
|
||||
func (app *Application) handleRoot(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write([]byte("Welcome to the EHR integration application!"))
|
||||
}
|
||||
|
||||
func getWellKnownUrl(iss string) (string, error) {
|
||||
wellKnownUrl := fmt.Sprintf("%s/.well-known/smart-configuration", iss)
|
||||
resp, err := http.Get(wellKnownUrl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("failed to fetch well-known URL")
|
||||
}
|
||||
|
||||
var bodyBytes []byte
|
||||
bodyBytes, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(bodyBytes), nil
|
||||
|
||||
}
|
||||
|
||||
// handleLaunch processes the SMART on FHIR launch request.
|
||||
func (app *Application) handleLaunch(w http.ResponseWriter, r *http.Request) {
|
||||
launchID := r.URL.Query().Get("launch")
|
||||
iss := r.URL.Query().Get("iss")
|
||||
// Log the query parameters and body data for debugging
|
||||
log.Printf("Query parameters: %v", r.URL.Query())
|
||||
if launchID == "" || iss == "" {
|
||||
http.Error(w, "Missing launch or iss parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the FHIR server base URL (iss)
|
||||
_, err := url.ParseRequestURI(iss)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid FHIR server base URL (iss)", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
//Make sure the FHIR server base URL (iss) is registered in the application's configuration
|
||||
var ehrConfig *EHRClientsConfig
|
||||
for _, ehr := range app.config.eHRs {
|
||||
if ehr.url == iss {
|
||||
ehrConfig = &ehr
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ehrConfig == nil {
|
||||
http.Error(w, "Unsupported FHIR server base URL (iss)", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var clientID string
|
||||
var clientSecret string
|
||||
clientID = ehrConfig.clientID
|
||||
clientSecret = ehrConfig.clientSecret
|
||||
|
||||
if clientID == "" || clientSecret == "" {
|
||||
http.Error(w, "Missing client ID or client secret", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
wellKnownBody, err := getWellKnownUrl(iss)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to fetch SMART configuration from FHIR server", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Get the authorization endpoint from the wellKnownBody and use it to construct the authorization URL
|
||||
var discoveryDoc map[string]interface{}
|
||||
if err := json.NewDecoder(strings.NewReader(wellKnownBody)).Decode(&discoveryDoc); err != nil {
|
||||
http.Error(w, "Invalid SMART configuration response", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
authEndpoint, ok := discoveryDoc["authorization_endpoint"].(string)
|
||||
if !ok || authEndpoint == "" {
|
||||
http.Error(w, "Authorization endpoint not found in SMART configuration", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate a state value that can be used to identify the EHR client and can be used to prevent CSRF attacks.
|
||||
state := iss + "state_hash" + launchID
|
||||
|
||||
// Construct the authorization URL
|
||||
authURL := fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&launch=%s&scope=openid+profile+launch+patient/*.read&state=%s&aud=%s",
|
||||
authEndpoint, clientID, url.QueryEscape(app.config.smart.redirectPath), launchID, state, iss)
|
||||
|
||||
http.Redirect(w, r, authURL, http.StatusFound)
|
||||
}
|
||||
|
||||
// handleAuthRedirect processes the authorization redirect and retrieves patient details.
|
||||
func (app *Application) handleAuthRedirect(w http.ResponseWriter, r *http.Request) {
|
||||
// Log the query parameters and body data for debugging
|
||||
log.Printf("Query parameters: %v", r.URL.Query())
|
||||
|
||||
bodyData := make(map[string]interface{})
|
||||
if err := json.NewDecoder(r.Body).Decode(&bodyData); err != nil {
|
||||
log.Printf("Failed to decode body data: %v", err)
|
||||
} else {
|
||||
log.Printf("Body data: %v", bodyData)
|
||||
}
|
||||
code := r.URL.Query().Get("code")
|
||||
state := r.URL.Query().Get("state")
|
||||
|
||||
if code == "" || state == "" {
|
||||
http.Error(w, "Missing code or state parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Extract the EHR client ID from the state parameter
|
||||
stateParts := strings.Split(state, "state_hash")
|
||||
if len(stateParts) != 2 {
|
||||
http.Error(w, "Invalid state parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
ehr_iss := stateParts[0]
|
||||
// Fetch the EHR client configuration based on the EHR client ID
|
||||
var ehrConfig *EHRClientsConfig
|
||||
for _, ehr := range app.config.eHRs {
|
||||
if ehr.url == ehr_iss {
|
||||
ehrConfig = &ehr
|
||||
break
|
||||
}
|
||||
}
|
||||
if ehrConfig == nil {
|
||||
http.Error(w, "Unsupported EHR client", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
wellKnownBody, err := getWellKnownUrl(ehr_iss)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to fetch SMART configuration from FHIR server", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Construct the token request URL from the well-knownBody
|
||||
var discoveryDoc map[string]interface{}
|
||||
if err := json.NewDecoder(strings.NewReader(wellKnownBody)).Decode(&discoveryDoc); err != nil {
|
||||
http.Error(w, "Invalid SMART configuration response", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
tokenEndpoint, ok := discoveryDoc["token_endpoint"].(string)
|
||||
if !ok || tokenEndpoint == "" {
|
||||
http.Error(w, "Token endpoint not found in SMART configuration", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Construct the token request URL from the well-knownBody
|
||||
// Prepare form data for token request
|
||||
formData := url.Values{
|
||||
"grant_type": {"authorization_code"},
|
||||
"code": {code},
|
||||
"redirect_uri": {app.config.smart.redirectPath},
|
||||
}
|
||||
|
||||
// Create request with Basic authentication
|
||||
req, err := http.NewRequest("POST", tokenEndpoint, strings.NewReader(formData.Encode()))
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to exchange authorization code for token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Add required headers
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.SetBasicAuth(ehrConfig.clientID, ehrConfig.clientSecret)
|
||||
|
||||
// Make the token request
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("Failed to make token request: %v", err)
|
||||
http.Error(w, "Failed to exchange authorization code for token: ", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Printf("Failed to exchange authorization code for token: %v", resp.Status)
|
||||
|
||||
// Decode and log the response body
|
||||
var responseBody map[string]interface{}
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
if err := json.Unmarshal(bodyBytes, &responseBody); err != nil {
|
||||
log.Printf("Failed to decode token response body: %v", err)
|
||||
} else {
|
||||
log.Printf("Token response: %v", responseBody)
|
||||
}
|
||||
// Reset the response body for later use
|
||||
resp.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
http.Error(w, "Failed to exchange authorization code for token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Parse the token response
|
||||
var tokenResponse map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
|
||||
http.Error(w, "Invalid token response", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
//log the token response
|
||||
log.Printf("Token response: %v", tokenResponse)
|
||||
// Extract the access token and patient ID from the token response
|
||||
accessToken, ok := tokenResponse["access_token"].(string)
|
||||
if !ok || accessToken == "" {
|
||||
http.Error(w, "Access token not found in token response", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
patientID, ok := tokenResponse["patient"].(string)
|
||||
if !ok || patientID == "" {
|
||||
http.Error(w, "Patient ID not found in token response", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Fetch patient details from the EHR using the access token
|
||||
patientURL := fmt.Sprintf("%s/Patient/%s", ehrConfig.url, patientID)
|
||||
|
||||
req, err = http.NewRequest("GET", patientURL, nil)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create patient details request", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to fetch patient details", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
http.Error(w, "Failed to fetch patient details", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Parse the patient details response
|
||||
var patientDetails map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&patientDetails); err != nil {
|
||||
http.Error(w, "Invalid patient details response", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Log the patient details for debugging purposes
|
||||
log.Printf("Patient details: %v", patientDetails)
|
||||
// Render the patient details in the response
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
// Create HTML table header
|
||||
html := "<html><head><style>table,th,td {border: 1px solid black; border-collapse: collapse; padding: 5px;}</style></head><body>"
|
||||
html += "<h2>Patient Details</h2><table>"
|
||||
|
||||
// Recursively build table rows from JSON
|
||||
var buildTableRows func(data map[string]interface{}, indent string) string
|
||||
buildTableRows = func(data map[string]interface{}, indent string) string {
|
||||
var rows string
|
||||
for key, value := range data {
|
||||
rows += "<tr>"
|
||||
rows += fmt.Sprintf("<td><strong>%s%s</strong></td>", indent, key)
|
||||
|
||||
switch v := value.(type) {
|
||||
case map[string]interface{}:
|
||||
rows += "<td>" + buildTableRows(v, indent+" ") + "</td>"
|
||||
case []interface{}:
|
||||
rows += "<td><table>"
|
||||
for _, item := range v {
|
||||
if m, ok := item.(map[string]interface{}); ok {
|
||||
rows += buildTableRows(m, indent+" ")
|
||||
} else {
|
||||
rows += fmt.Sprintf("<tr><td>%v</td></tr>", item)
|
||||
}
|
||||
}
|
||||
rows += "</table></td>"
|
||||
default:
|
||||
rows += fmt.Sprintf("<td>%v</td>", v)
|
||||
}
|
||||
rows += "</tr>"
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
html += buildTableRows(patientDetails, "")
|
||||
html += "</table></body></html>"
|
||||
|
||||
w.Write([]byte(html))
|
||||
}
|
||||
0
conf/application.conf
Normal file
0
conf/application.conf
Normal file
Reference in New Issue
Block a user