working frontend. adding database.

This commit is contained in:
Jason Kulatunga 2022-08-25 18:26:29 -07:00
parent 1b77e3d01b
commit e657d73e0e
11 changed files with 1298 additions and 63 deletions

View File

@ -0,0 +1,12 @@
package database
import (
"context"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/models"
)
type DatabaseRepository interface {
Close() error
CreateProviderCredentials(ctx context.Context, providerCreds models.ProviderCredential) error
}

View File

@ -0,0 +1,86 @@
package database
import (
"context"
"fmt"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/config"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/models"
"github.com/glebarez/sqlite"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
"net/url"
)
func NewRepository(appConfig config.Interface, globalLogger logrus.FieldLogger) (DatabaseRepository, error) {
//backgroundContext := context.Background()
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Gorm/SQLite setup
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
globalLogger.Infof("Trying to connect to sqlite db: %s\n", appConfig.GetString("web.database.location"))
// When a transaction cannot lock the database, because it is already locked by another one,
// SQLite by default throws an error: database is locked. This behavior is usually not appropriate when
// concurrent access is needed, typically when multiple processes write to the same database.
// PRAGMA busy_timeout lets you set a timeout or a handler for these events. When setting a timeout,
// SQLite will try the transaction multiple times within this timeout.
// fixes #341
// https://rsqlite.r-dbi.org/reference/sqlitesetbusyhandler
// retrying for 30000 milliseconds, 30seconds - this would be unreasonable for a distributed multi-tenant application,
// but should be fine for local usage.
pragmaStr := sqlitePragmaString(map[string]string{
"busy_timeout": "30000",
})
database, err := gorm.Open(sqlite.Open(appConfig.GetString("web.database.location")+pragmaStr), &gorm.Config{
//TODO: figure out how to log database queries again.
//Logger: logger
DisableForeignKeyConstraintWhenMigrating: true,
})
if err != nil {
return nil, fmt.Errorf("Failed to connect to database! - %v", err)
}
globalLogger.Infof("Successfully connected to scrutiny sqlite db: %s\n", appConfig.GetString("web.database.location"))
deviceRepo := sqliteRepository{
appConfig: appConfig,
logger: globalLogger,
gormClient: database,
}
return &deviceRepo, nil
}
type sqliteRepository struct {
appConfig config.Interface
logger logrus.FieldLogger
gormClient *gorm.DB
}
func (sr *sqliteRepository) Close() error {
return nil
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// DeviceSummary
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (sr *sqliteRepository) CreateProviderCredentials(ctx context.Context, providerCreds models.ProviderCredential) error {
return sr.gormClient.WithContext(ctx).Create(providerCreds).Error
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Utilities
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func sqlitePragmaString(pragmas map[string]string) string {
q := url.Values{}
for key, val := range pragmas {
q.Add("_pragma", key+"="+val)
}
queryStr := q.Encode()
if len(queryStr) > 0 {
return "?" + queryStr
}
return ""
}

View File

@ -0,0 +1,33 @@
package models
import "gorm.io/gorm"
type ProviderCredential struct {
gorm.Model
OauthEndpointBaseUrl string `json:"oauth_endpoint_base_url"`
ApiEndpointBaseUrl string `json:"api_endpoint_base_url"`
ClientId string `json:"client_id"`
RedirectUri string `json:"redirect_uri"`
Scopes []string `json:"scopes"`
PatientId string `json:"patient"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
IdToken string `json:"id_token"`
ExpiresAt string `json:"expires_at"`
CodeChallenge string `json:"code_challenge"`
CodeVerifier string `json:"code_verifier"`
}
/*
serverUrl: connectData.message.api_endpoint_base_url,
clientId: connectData.message.client_id,
redirectUri: connectData.message.redirect_uri,
tokenUri: `${connectData.message.oauth_endpoint_base_url}/token`,
scope: connectData.message.scopes.join(' '),
tokenResponse: payload,
expiresAt: getAccessTokenExpiration(payload, new BrowserAdapter()),
codeChallenge: codeChallenge,
codeVerifier: codeVerifier
*/

View File

@ -0,0 +1,7 @@
package models
import "gorm.io/gorm"
type User struct {
gorm.Model
}

View File

@ -0,0 +1,29 @@
package handler
import (
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/database"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/models"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"net/http"
)
func CreateProviderCredentials(c *gin.Context) {
logger := c.MustGet("LOGGER").(*logrus.Entry)
databaseRepo := c.MustGet("REPOSITORY").(database.DatabaseRepository)
var providerCred models.ProviderCredential
if err := c.ShouldBindJSON(&providerCred); err != nil {
logger.Errorln("An error occurred while parsing posted provider credential", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false})
return
}
err := databaseRepo.CreateProviderCredentials(c, providerCred)
if err != nil {
logger.Errorln("An error occurred while storing provider credential", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": providerCred})
}

View File

@ -0,0 +1,22 @@
package middleware
import (
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/config"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/database"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
func RepositoryMiddleware(appConfig config.Interface, globalLogger logrus.FieldLogger) gin.HandlerFunc {
deviceRepo, err := database.NewRepository(appConfig, globalLogger)
if err != nil {
panic(err)
}
//TODO: determine where we can call defer deviceRepo.Close()
return func(c *gin.Context) {
c.Set("REPOSITORY", deviceRepo)
c.Next()
}
}

View File

@ -20,7 +20,7 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
r := gin.New()
r.Use(middleware.LoggerMiddleware(logger))
//r.Use(middleware.DatabaseMiddleware(ae.Config, logger))
r.Use(middleware.RepositoryMiddleware(ae.Config, logger))
r.Use(middleware.ConfigMiddleware(ae.Config))
r.Use(gin.Recovery())
@ -39,7 +39,7 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
"success": true,
})
})
api.POST("/hello-world", handler.GetHelloWorld) //used to save settings
api.POST("/provider_credentials", handler.CreateProviderCredentials) //used to save settings
}
}

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@
"@angular/platform-browser-dynamic": "^14.1.3",
"@angular/router": "^14.1.3",
"@panva/oauth4webapi": "^1.1.3",
"fhirclient": "^2.5.1",
"rxjs": "~6.5.4",
"tslib": "^2.0.0",
"zone.js": "~0.11.8"

View File

@ -2,6 +2,16 @@ import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
// import 'fhir-js-client';
import * as Oauth from '@panva/oauth4webapi';
import { concatMap, delay, retryWhen } from 'rxjs/operators';
import { Observable, of, throwError } from 'rxjs';
// import {fhirclient} from 'fhirclient/lib/types';
import * as FHIR from "fhirclient"
import Client from 'fhirclient/lib/Client';
import {getAccessTokenExpiration} from 'fhirclient/lib/lib';
import BrowserAdapter from 'fhirclient/lib/adapters/BrowserAdapter';
export const retryCount = 24; //wait 2 minutes (5 * 24 = 120)
export const retryWaitMilliSeconds = 5000; //wait 5 seconds
@Component({
selector: 'app-root',
@ -16,8 +26,8 @@ export class AppComponent {
connect(provider: string) {
this.http.get<any>(`https://sandbox-api.fastenhealth.com/v1/connect/${provider}`)
.subscribe(async (data: any) => {
console.log(data);
.subscribe(async (connectData: any) => {
console.log(connectData);
// https://github.com/panva/oauth4webapi/blob/8eba19eac408bdec5c1fe8abac2710c50bfadcc3/examples/public.ts
const codeVerifier = Oauth.generateRandomCodeVerifier();
@ -25,22 +35,128 @@ export class AppComponent {
const codeChallengeMethod = 'S256';
// generate the authorization url
const authorizationUrl = new URL(`${data.message.server_url}/authorize`);
authorizationUrl.searchParams.set('client_id', data.message.client_id);
const state = this.uuidV4()
const authorizationUrl = new URL(`${connectData.message.oauth_endpoint_base_url}/authorize`);
authorizationUrl.searchParams.set('client_id', connectData.message.client_id);
authorizationUrl.searchParams.set('code_challenge', codeChallenge);
authorizationUrl.searchParams.set('code_challenge_method', codeChallengeMethod);
authorizationUrl.searchParams.set('redirect_uri', data.message.redirect_uri);
authorizationUrl.searchParams.set('redirect_uri', connectData.message.redirect_uri);
authorizationUrl.searchParams.set('response_type', 'code');
authorizationUrl.searchParams.set('scope', data.message.scopes.join(' '));
authorizationUrl.searchParams.set('state', 'hello-world-my-friend');
if (data.message.aud){
authorizationUrl.searchParams.set('aud', data.message.aud);
authorizationUrl.searchParams.set('scope', connectData.message.scopes.join(' '));
authorizationUrl.searchParams.set('state', state);
if (connectData.message.aud){
authorizationUrl.searchParams.set('aud', connectData.message.aud);
}
console.log('authorize url:', authorizationUrl.toString());
// open new browser window
window.open(authorizationUrl.toString(), "_blank");
//wait for response
this.waitForClaimOrTimeout(provider, state).subscribe(async (claimData: any) => {
console.log("claim response:", claimData)
//swap code for token
let sub: string
let access_token: string
// @ts-expect-error
const client: oauth.Client = {
client_id: connectData.message.client_id,
token_endpoint_auth_method: 'none',
}
const as = {
issuer: `${authorizationUrl.protocol}//${authorizationUrl.host}`,
authorization_endpoint: `${connectData.message.oauth_endpoint_base_url}/authorize`,
token_endpoint: `${connectData.message.oauth_endpoint_base_url}/token`,
introspect_endpoint: `${connectData.message.oauth_endpoint_base_url}/introspect`,
}
console.log("STARTING--- Oauth.validateAuthResponse")
const params = Oauth.validateAuthResponse(as, client, new URLSearchParams(claimData.message), state)
if (Oauth.isOAuth2Error(params)) {
console.log('error', params)
throw new Error() // Handle OAuth 2.0 redirect error
}
console.log("ENDING--- Oauth.validateAuthResponse")
console.log("STARTING--- Oauth.authorizationCodeGrantRequest")
const response = await Oauth.authorizationCodeGrantRequest(
as,
client,
params,
connectData.message.redirect_uri,
codeVerifier,
)
const payload = await response.json()
console.log("ENDING--- Oauth.authorizationCodeGrantRequest", payload)
//Create FHIR Client
const clientState = {
serverUrl: connectData.message.api_endpoint_base_url,
clientId: connectData.message.client_id,
redirectUri: connectData.message.redirect_uri,
tokenUri: `${connectData.message.oauth_endpoint_base_url}/token`,
scope: connectData.message.scopes.join(' '),
tokenResponse: payload,
expiresAt: getAccessTokenExpiration(payload, new BrowserAdapter()),
codeChallenge: codeChallenge,
codeVerifier: codeVerifier
}
console.log("STARTING--- FHIR.client(clientState)", clientState)
const fhirClient = FHIR.client(clientState);
console.log("STARTING--- client.request(Patient)")
const patientResponse = await fhirClient.request("PatientAccess/v1/$userinfo")
console.log(patientResponse)
// // fetch userinfo response
//
// const response = await oauth.userInfoRequest(as, client, access_token)
//
// let challenges: oauth.WWWAuthenticateChallenge[] | undefined
// if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) {
// for (const challenge of challenges) {
// console.log('challenge', challenge)
// }
// throw new Error() // Handle www-authenticate challenges as needed
// }
//
// const result = await oauth.processUserInfoResponse(as, client, sub, response)
// console.log('result', result)
})
});
}
waitForClaimOrTimeout(provider: string, state: string): Observable<any> {
return this.http.get<any>(`https://sandbox-api.fastenhealth.com/v1/claim/${provider}`, {params: {"state": state}}).pipe(
retryWhen(error =>
error.pipe(
concatMap((error, count) => {
if (count <= retryCount && error.status == 500) {
return of(error);
}
return throwError(error);
}),
delay(retryWaitMilliSeconds)
)
)
)
}
uuidV4(){
// @ts-ignore
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
}

11
go.mod
View File

@ -5,9 +5,11 @@ go 1.18
require (
github.com/analogj/go-util v0.0.0-20210417161720-39b497cca03b
github.com/gin-gonic/gin v1.8.1
github.com/glebarez/sqlite v1.4.6
github.com/sirupsen/logrus v1.9.0
github.com/spf13/viper v1.12.0
github.com/urfave/cli/v2 v2.11.2
gorm.io/gorm v1.23.8
)
require (
@ -15,11 +17,15 @@ require (
github.com/fatih/color v1.13.0 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.17.3 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.0 // indirect
github.com/goccy/go-json v0.9.7 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.4 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
@ -32,6 +38,7 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cast v1.5.0 // indirect
@ -49,4 +56,8 @@ require (
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.16.8 // indirect
modernc.org/mathutil v1.4.1 // indirect
modernc.org/memory v1.1.1 // indirect
modernc.org/sqlite v1.17.3 // indirect
)