Skip to content

Commit

Permalink
Merge main
Browse files Browse the repository at this point in the history
Picked up on most recent change with DeleteSource func
  • Loading branch information
nickmurray47 committed Oct 16, 2023
2 parents 8be1e34 + 79c3e85 commit d154d9e
Show file tree
Hide file tree
Showing 168 changed files with 9,152 additions and 853 deletions.
17 changes: 0 additions & 17 deletions .idea/dataSources.xml

This file was deleted.

3 changes: 2 additions & 1 deletion backend/cmd/fasten/fasten.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ var goos string
var goarch string

func main() {

log.Print("Starting fasten-onprem")
defer log.Print("Finished fasten-onprem")
appconfig, err := config.Create()
if err != nil {
fmt.Printf("FATAL: %+v\n", err)
Expand Down
56 changes: 56 additions & 0 deletions backend/pkg/database/gorm_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,62 @@ func (gr *GormRepository) GetSources(ctx context.Context) ([]models.SourceCreden
return sourceCreds, results.Error
}

func (gr *GormRepository) DeleteSource(ctx context.Context, sourceId string) (int64, error) {
currentUser, currentUserErr := gr.GetCurrentUser(ctx)
if currentUserErr != nil {
return 0, currentUserErr
}

if strings.TrimSpace(sourceId) == "" {
return 0, fmt.Errorf("sourceId cannot be blank")
}
//delete all resources for this source
sourceUUID, err := uuid.Parse(sourceId)
if err != nil {
return 0, err
}

rowsEffected := int64(0)
resourceTypes := databaseModel.GetAllowedResourceTypes()
for _, resourceType := range resourceTypes {
tableName, err := databaseModel.GetTableNameByResourceType(resourceType)
if err != nil {
return 0, err
}
results := gr.GormClient.WithContext(ctx).
Where(models.OriginBase{
UserID: currentUser.ID,
SourceID: sourceUUID,
}).
Table(tableName).
Delete(&models.ResourceBase{})
rowsEffected += results.RowsAffected
if results.Error != nil {
return rowsEffected, results.Error
}
}

//delete relatedResources entries
results := gr.GormClient.WithContext(ctx).
Where(models.RelatedResource{ResourceBaseUserID: currentUser.ID, ResourceBaseSourceID: sourceUUID}).
Delete(&models.RelatedResource{})
if results.Error != nil {
return rowsEffected, results.Error
}

//soft delete the source credential
results = gr.GormClient.WithContext(ctx).
Where(models.SourceCredential{
ModelBase: models.ModelBase{
ID: sourceUUID,
},
UserID: currentUser.ID,
}).
Delete(&models.SourceCredential{})
rowsEffected += results.RowsAffected
return rowsEffected, results.Error
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Background Job
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Expand Down
1 change: 1 addition & 0 deletions backend/pkg/database/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type DatabaseRepository interface {
GetSourceSummary(context.Context, string) (*models.SourceSummary, error)
GetSources(context.Context) ([]models.SourceCredential, error)
UpdateSource(ctx context.Context, sourceCreds *models.SourceCredential) error
DeleteSource(ctx context.Context, sourceId string) (int64, error)

CreateGlossaryEntry(ctx context.Context, glossaryEntry *models.Glossary) error
GetGlossaryEntry(ctx context.Context, code string, codeSystem string) (*models.Glossary, error)
Expand Down
84 changes: 84 additions & 0 deletions backend/pkg/models/source_credential.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package models

import (
"bytes"
"encoding/json"
"fmt"
"github.com/fastenhealth/fasten-onprem/backend/pkg/jwk"
Expand Down Expand Up @@ -134,7 +135,90 @@ func (s *SourceCredential) IsDynamicClient() bool {
return len(s.DynamicClientRegistrationMode) > 0
}

// This method will generate a new keypair, register a new dynamic client with the provider
// it will set the following fields:
// - DynamicClientJWKS
// - DynamicClientId
func (s *SourceCredential) RegisterDynamicClient() error {

//this source requires dynamic client registration
// see https://fhir.epic.com/Documentation?docId=Oauth2&section=Standalone-Oauth2-OfflineAccess-0

// Generate a public-private key pair
// Must be 2048 bits (larger keys will silently fail when used with Epic, untested on other providers)
sourceSpecificClientKeyPair, err := jwk.JWKGenerate()
if err != nil {
return fmt.Errorf("an error occurred while generating device-specific keypair for dynamic client: %w", err)
}

//store in sourceCredential
serializedKeypair, err := jwk.JWKSerialize(sourceSpecificClientKeyPair)
if err != nil {
return fmt.Errorf("an error occurred while serializing keypair for dynamic client: %w", err)
}
s.DynamicClientJWKS = []map[string]string{
serializedKeypair,
}

//generate dynamic client registration request
payload := ClientRegistrationRequest{
SoftwareId: s.ClientId,
Jwks: ClientRegistrationRequestJwks{
Keys: []ClientRegistrationRequestJwksKey{
{
KeyType: "RSA",
KeyId: serializedKeypair["kid"],
Modulus: serializedKeypair["n"],
PublicExponent: serializedKeypair["e"],
},
},
},
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("an error occurred while marshalling dynamic client registration request: %w", err)
}

//http.Post("https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token", "application/x-www-form-urlencoded", bytes.NewBuffer([]byte(fmt.Sprintf("grant_type=client_credentials&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=%s&scope=system/Patient.read", sourceSpecificClientKeyPair))))
req, err := http.NewRequest(http.MethodPost, s.RegistrationEndpoint, bytes.NewBuffer(payloadBytes))
if err != nil {
return fmt.Errorf("an error occurred while generating dynamic client registration request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.AccessToken))

registrationResponse, err := http.DefaultClient.Do(req)

if err != nil {
return fmt.Errorf("an error occurred while sending dynamic client registration request: %w", err)
}
defer registrationResponse.Body.Close()
if registrationResponse.StatusCode >= 300 || registrationResponse.StatusCode < 200 {
b, err := io.ReadAll(registrationResponse.Body)
if err == nil {
log.Printf("Error Response body: %s", string(b))
}
return fmt.Errorf("an error occurred while reading dynamic client registration response, status code was not 200: %d", registrationResponse.StatusCode)

}

//read response
var registrationResponseBytes ClientRegistrationResponse
err = json.NewDecoder(registrationResponse.Body).Decode(&registrationResponseBytes)
if err != nil {
return fmt.Errorf("an error occurred while parsing dynamic client registration response: %w", err)
}

//store the dynamic client id
s.DynamicClientId = registrationResponseBytes.ClientId
return nil
}

// this will set/update the AccessToken and Expiry using the dynamic client credentials
// it will set the following fields:
// - AccessToken
// - ExpiresAt
func (s *SourceCredential) RefreshDynamicClientAccessToken() error {
if len(s.DynamicClientRegistrationMode) == 0 {
return fmt.Errorf("dynamic client registration mode not set")
Expand Down
96 changes: 52 additions & 44 deletions backend/pkg/web/handler/dashboard/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,53 @@
"description": "An example dashboard to show-off the power of Fasten widgets",
"widgets": [
{
"title_text": "Diabetes Tracking",
"title_text": "Records Summary",
"description_text": "Track key metrics for your chronic disease (eg. Diabetes). The data within this widget is not reflective of your health record, and is only present for demonstrational purposes.",
"x": 0,
"y": 0,
"width": 8,
"height": 6,
"item_type": "records-summary-widget"
},
{
"title_text": "Patient Vitals",
"description_text": "",
"x": 8,
"y": 0,
"width": 4,
"height": 6,
"item_type": "patient-vitals-widget"
},

{
"title_text": "Observations by Type",
"description_text": "",
"x": 0,
"y": 6,
"width": 8,
"height": 5,
"item_type": "complex-line-widget"
"item_type": "donut-chart-widget",
"queries": [{
"q": {
"select": [],
"from": "Observation",
"where": {},
"aggregations":{
"count_by": {"field": "code:code" }
}
}
}],
"parsing": {
"label": "label",
"key": "value"
}
},

{
"title_text": "Weight",
"description_text": "",
"x": 8,
"y": 0,
"y": 6,
"width": 2,
"height": 2,
"item_type": "simple-line-chart-widget",
Expand All @@ -31,7 +65,8 @@
"from": "Observation",
"where": {
"code": "http://loinc.org|29463-7,http://loinc.org|3141-9,http://snomed.info/sct|27113001"
}
},
"limit": 50
}
}],
"parsing": {
Expand All @@ -43,7 +78,7 @@
"title_text": "Height",
"description_text": "",
"x": 10,
"y": 0,
"y": 6,
"width": 2,
"height": 2,
"item_type": "simple-line-chart-widget",
Expand All @@ -57,7 +92,8 @@
"from": "Observation",
"where": {
"code": "http://loinc.org|8302-2"
}
},
"limit": 50
}
}],
"parsing": {
Expand All @@ -69,7 +105,7 @@
"title_text": "Blood Pressure",
"description_text": "How much pressure your blood is exerting against your artery walls when the heart beats",
"x": 8,
"y": 2,
"y": 8,
"width": 4,
"height": 3,
"item_type": "grouped-bar-chart-widget",
Expand All @@ -83,7 +119,8 @@
"from": "Observation",
"where": {
"componentCode": "http://loinc.org|8462-4"
}
},
"limit": 50
},
"dataset_options": {
"label": "Diastolic"
Expand All @@ -98,7 +135,8 @@
"from": "Observation",
"where": {
"componentCode": "http://loinc.org|8480-6"
}
},
"limit": 50
},
"dataset_options": {
"label": "Systolic"
Expand All @@ -109,43 +147,12 @@
"yAxisKey": "data"
}
},
{
"title_text": "Patient Vitals",
"description_text": "",
"x": 0,
"y": 5,
"width": 4,
"height": 5,
"item_type": "patient-vitals-widget"
},
{
"title_text": "Observations by Type",
"description_text": "",
"x": 4,
"y": 5,
"width": 8,
"height": 5,
"item_type": "donut-chart-widget",
"queries": [{
"q": {
"select": [],
"from": "Observation",
"where": {},
"aggregations":{
"count_by": {"field": "code:code" }
}
}
}],
"parsing": {
"label": "label",
"key": "value"
}
},

{
"title_text": "Compliance",
"description_text": "Use to track important healthcare and medical tasks.",
"x": 0,
"y": 10,
"y": 11,
"width": 4,
"height": 2,
"item_type": "dual-gauges-widget",
Expand Down Expand Up @@ -184,7 +191,7 @@
"title_text": "Recent Encounters",
"description_text": "Recent interactions with healthcare providers",
"x": 4,
"y": 10,
"y": 11,
"width": 8,
"height": 4,
"item_type": "table-widget",
Expand All @@ -197,7 +204,8 @@
"participant.individual.display as provider"
],
"from": "Encounter",
"where": {}
"where": {},
"limit": 50
}
}],
"parsing": {
Expand Down
Loading

0 comments on commit d154d9e

Please sign in to comment.