adding a new configuration for "cloud_sandbox" - used for AWS bucket.

consistent couchdb creds.
removed metadata endpoint and config.
ensure that the couchdb endpoint can be relative or absolute url. added tests for relative vs abs urls.
This commit is contained in:
Jason Kulatunga 2022-10-28 19:01:20 -07:00
parent 3a5db5452a
commit f0f37cd738
21 changed files with 180 additions and 92 deletions

View File

@ -26,7 +26,7 @@ jobs:
- run: |
make frontend-dep
cd frontend
yarn run build -- --configuration sandbox --output-path=../dist
yarn run build -- --configuration cloud_sandbox --output-path=../dist
- name: Configure AWS credentials using delegated access
uses: aws-actions/configure-aws-credentials@v1

View File

@ -26,11 +26,11 @@ func (c *configuration) Init() error {
c.SetDefault("web.listen.basepath", "")
c.SetDefault("web.src.frontend.path", "/opt/fasten/web")
c.SetDefault("web.couchdb.scheme", "http")
c.SetDefault("web.couchdb.host", "localhost")
c.SetDefault("web.couchdb.port", "5984")
c.SetDefault("web.couchdb.admin_username", "admin")
c.SetDefault("web.couchdb.admin_password", "mysecretpassword")
c.SetDefault("couchdb.scheme", "http")
c.SetDefault("couchdb.host", "localhost")
c.SetDefault("couchdb.port", "5984")
c.SetDefault("couchdb.admin.username", "admin")
c.SetDefault("couchdb.admin.password", "mysecretpassword")
c.SetDefault("log.level", "INFO")
c.SetDefault("log.file", "")

View File

@ -15,7 +15,7 @@ func NewRepository(appConfig config.Interface, globalLogger logrus.FieldLogger)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Couchdb setup
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
couchdbUrl := fmt.Sprintf("%s://%s:%s", appConfig.GetString("web.couchdb.scheme"), appConfig.GetString("web.couchdb.host"), appConfig.GetString("web.couchdb.port"))
couchdbUrl := fmt.Sprintf("%s://%s:%s", appConfig.GetString("couchdb.scheme"), appConfig.GetString("couchdb.host"), appConfig.GetString("couchdb.port"))
globalLogger.Infof("Trying to connect to couchdb: %s\n", couchdbUrl)
@ -26,8 +26,8 @@ func NewRepository(appConfig config.Interface, globalLogger logrus.FieldLogger)
err = database.Authenticate(context.Background(),
couchdb.BasicAuth(
appConfig.GetString("web.couchdb.admin_username"),
appConfig.GetString("web.couchdb.admin_password")),
appConfig.GetString("couchdb.admin.username"),
appConfig.GetString("couchdb.admin.password")),
)
if err != nil {

View File

@ -14,7 +14,7 @@ import (
func CouchDBProxy(c *gin.Context) {
appConfig := c.MustGet("CONFIG").(config.Interface)
couchdbUrl := fmt.Sprintf("%s://%s:%s", appConfig.GetString("web.couchdb.scheme"), appConfig.GetString("web.couchdb.host"), appConfig.GetString("web.couchdb.port"))
couchdbUrl := fmt.Sprintf("%s://%s:%s", appConfig.GetString("couchdb.scheme"), appConfig.GetString("couchdb.host"), appConfig.GetString("couchdb.port"))
remote, err := url.Parse(couchdbUrl)
if err != nil {
panic(err)

View File

@ -1,48 +0,0 @@
package handler
import (
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/models"
"github.com/gin-gonic/gin"
"net/http"
)
func GetMetadataSource(c *gin.Context) {
metadataSource := map[string]models.MetadataSource{
string(pkg.SourceTypeAthena): {Display: "Athena (Sandbox)", SourceType: pkg.SourceTypeAthena, Category: []string{"Sandbox"}, Supported: true},
string(pkg.SourceTypeEpic): {Display: "Epic (Sandbox)", SourceType: pkg.SourceTypeEpic, Category: []string{"Sandbox"}, Supported: true},
string(pkg.SourceTypeLogica): {Display: "Logica (Sandbox)", SourceType: pkg.SourceTypeLogica, Category: []string{"Sandbox"}, Supported: true},
string(pkg.SourceTypeHealthIT): {Display: "HealthIT (Sandbox)", SourceType: pkg.SourceTypeHealthIT, Category: []string{"Sandbox"}, Supported: true},
//fails with CORS error when swapping token. Must be a confidential client.
string(pkg.SourceTypeCareEvolution): {Display: "CareEvolution (Sandbox)", SourceType: pkg.SourceTypeCareEvolution, Category: []string{"Sandbox"}, Supported: false},
// enabled
string(pkg.SourceTypeAetna): {Display: "Aetna", SourceType: pkg.SourceTypeAetna, Category: []string{"Insurance"}, Supported: true},
string(pkg.SourceTypeCigna): {Display: "Cigna", SourceType: pkg.SourceTypeCigna, Category: []string{"Insurance", "Hospital"}, Supported: true},
string(pkg.SourceTypeBlueButtonMedicare): {Display: "Medicare/VA Health (BlueButton)", SourceType: pkg.SourceTypeBlueButtonMedicare, Category: []string{"Hospital"}, Supported: true},
//TODO: infinite pagination for Encounters??
string(pkg.SourceTypeCerner): {Display: "Cerner (Sandbox)", SourceType: pkg.SourceTypeCerner, Category: []string{"Sandbox"}, Supported: true},
// pending
string(pkg.SourceTypeAnthem): {Display: "Anthem", SourceType: pkg.SourceTypeAnthem, Category: []string{"Insurance"}},
string(pkg.SourceTypeCedarSinai): {Display: "Cedar Sinai", SourceType: pkg.SourceTypeCedarSinai, Category: []string{"Hospital"}},
string(pkg.SourceTypeCommonSpirit): {Display: "Common Spirit", SourceType: pkg.SourceTypeCommonSpirit, Category: []string{"Hospital"}},
string(pkg.SourceTypeDeltaDental): {Display: "Delta Dental", SourceType: pkg.SourceTypeDeltaDental, Category: []string{"Insurance"}},
string(pkg.SourceTypeDignityHealth): {Display: "Dignity Health", SourceType: pkg.SourceTypeDignityHealth, Category: []string{"Hospital"}},
string(pkg.SourceTypeHCAHealthcare): {Display: "HCA Healthcare", SourceType: pkg.SourceTypeHCAHealthcare, Category: []string{"Insurance"}},
string(pkg.SourceTypeHumana): {Display: "Humana", SourceType: pkg.SourceTypeHumana, Category: []string{"Insurance"}},
string(pkg.SourceTypeKaiser): {Display: "Kaiser", SourceType: pkg.SourceTypeKaiser, Category: []string{"Hospital", "Insurance"}},
string(pkg.SourceTypeMetlife): {Display: "Metlife", SourceType: pkg.SourceTypeMetlife, Category: []string{"Insurance"}},
string(pkg.SourceTypeProvidence): {Display: "Providence", SourceType: pkg.SourceTypeProvidence, Category: []string{"Hospital"}},
string(pkg.SourceTypeStanford): {Display: "Stanford Healthcare", SourceType: pkg.SourceTypeStanford, Category: []string{"Hospital"}},
string(pkg.SourceTypeSutter): {Display: "Sutter", SourceType: pkg.SourceTypeSutter, Category: []string{"Hospital"}},
string(pkg.SourceTypeTrinity): {Display: "Trinity", SourceType: pkg.SourceTypeTrinity, Category: []string{"Hospital"}},
string(pkg.SourceTypeUCSF): {Display: "UCSF", SourceType: pkg.SourceTypeUCSF, Category: []string{"Hospital"}},
string(pkg.SourceTypeUnitedHealthcare): {Display: "United Healthcare", SourceType: pkg.SourceTypeUnitedHealthcare, Category: []string{"Insurance"}},
string(pkg.SourceTypeVerity): {Display: "Verity", SourceType: pkg.SourceTypeVerity, Category: []string{"Hospital"}},
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": metadataSource})
}

View File

@ -41,7 +41,6 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
})
api.POST("/auth/signup", handler.AuthSignup)
api.GET("/metadata/source", handler.GetMetadataSource)
r.Any("/database/*proxyPath", handler.CouchDBProxy)
r.GET("/cors/*proxyPath", handler.CORSProxy)

View File

@ -91,6 +91,33 @@
"maximumError": "10kb"
}
]
},
"cloud_sandbox": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.cloud_sandbox.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
}
}
@ -103,6 +130,12 @@
"configurations": {
"production": {
"browserTarget": "fastenhealth:build:production"
},
"sandbox": {
"browserTarget": "fastenhealth:build:sandbox"
},
"cloud_sandbox": {
"browserTarget": "fastenhealth:build:cloud_sandbox"
}
}
},

View File

@ -3,6 +3,7 @@ import {Source} from '../../../lib/models/database/source';
export class SourceSyncMessage {
source: Source
current_user: string
couchdb_endpoint_base: string
response?: any
}

View File

@ -19,6 +19,7 @@ import * as PouchCrypto from 'crypto-pouch';
PouchDB.plugin(PouchCrypto);
import PouchAuth from 'pouchdb-authentication'
import {PouchdbCrypto} from '../../lib/database/plugins/crypto';
import {environment} from '../../environments/environment';
PouchDB.plugin(PouchAuth);
@Injectable({
@ -27,7 +28,7 @@ PouchDB.plugin(PouchAuth);
export class FastenDbService extends PouchdbRepository {
constructor(private _httpClient: HttpClient) {
super();
super(environment.couchdb_endpoint_base);
}
@ -60,7 +61,16 @@ export class FastenDbService extends PouchdbRepository {
*/
public async Signup(newUser?: User): Promise<any> {
console.log("STARTING SIGNUP")
let resp = await this._httpClient.post<ResponseWrapper>(`${this.getBasePath()}/api/auth/signup`, newUser).toPromise()
let fastenApiEndpointBase = environment.fasten_api_endpoint_base
if (!(fastenApiEndpointBase.indexOf('http://') === 0 || fastenApiEndpointBase.indexOf('https://') === 0)){
//relative, we need to retrieve the absolutePath from base
fastenApiEndpointBase = this.GetEndpointAbsolutePath(globalThis.location,fastenApiEndpointBase)
}
let resp = await this._httpClient.post<ResponseWrapper>(`${fastenApiEndpointBase}/auth/signup`, newUser).toPromise()
console.log(resp)
return this.Signin(newUser.username, newUser.password);
}

View File

@ -6,6 +6,7 @@ import {SourceSyncMessage} from '../models/queue/source-sync-message';
import {ToastService} from '../services/toast.service';
import {ToastNotification, ToastType} from '../models/fasten/toast';
import {FastenDbService} from '../services/fasten-db.service';
import {environment} from '../../environments/environment';
@Injectable({
providedIn: 'root'
@ -19,6 +20,7 @@ export class QueueService {
const sourceSync = new SourceSyncMessage()
sourceSync.source = source
sourceSync.current_user = this.fastenDbService.current_user
sourceSync.couchdb_endpoint_base = environment.couchdb_endpoint_base
const input$: Observable<string> = of(JSON.stringify(sourceSync));
return fromWorker<string, string>(() => new Worker(new URL('./source-sync.worker', import.meta.url), {type: 'module'}), input$)
// .subscribe(message => {

View File

@ -18,7 +18,7 @@ export class SourceSyncWorker implements DoWork<string, string> {
console.log(msg); // outputs 'Hello from main thread'
const sourceSyncMessage = JSON.parse(msg) as SourceSyncMessage
const db = NewPouchdbRepositoryWebWorker(sourceSyncMessage.current_user)
const db = NewPouchdbRepositoryWebWorker(sourceSyncMessage.current_user, sourceSyncMessage.couchdb_endpoint_base)
const client = NewClient(sourceSyncMessage.source.source_type, new Source(sourceSyncMessage.source))
//TODO: validate the FHIR version from the datasource matches the client
// if the source token has been refreshed, we need to store it in the DB.

View File

@ -0,0 +1,10 @@
export const environment = {
production: true,
lighthouse_api_endpoint_base: 'https://lighthouse.fastenhealth.com/sandbox',
//used to specify the couchdb server that we're going to use (can be relative or absolute). Must not have trailing slash
couchdb_endpoint_base: 'https://couchdb.sandbox.fastenhealth.com',
//used to specify the api server that we're going to use (can be relative or absolute). Must not have trailing slash
fasten_api_endpoint_base: 'https://api.sandbox.fastenhealth.com/v1',
};

View File

@ -1,4 +1,10 @@
export const environment = {
production: true,
lighthouse_api_endpoint_base: 'https://lighthouse.fastenhealth.com/v1'
lighthouse_api_endpoint_base: 'https://lighthouse.fastenhealth.com/v1',
//used to specify the couchdb server that we're going to use (can be relative or absolute). Must not have trailing slash
couchdb_endpoint_base: '/database',
//used to specify the api server that we're going to use (can be relative or absolute). Must not have trailing slash
fasten_api_endpoint_base: '/api',
};

View File

@ -4,14 +4,11 @@
export const environment = {
production: true,
lighthouse_api_endpoint_base: 'https://lighthouse.fastenhealth.com/sandbox'
};
lighthouse_api_endpoint_base: 'https://lighthouse.fastenhealth.com/sandbox',
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
//used to specify the couchdb server that we're going to use (can be relative or absolute). Must not have trailing slash
couchdb_endpoint_base: '/database',
//used to specify the api server that we're going to use (can be relative or absolute). Must not have trailing slash
fasten_api_endpoint_base: '/api',
};

View File

@ -4,14 +4,17 @@
export const environment = {
production: false,
lighthouse_api_endpoint_base: 'https://lighthouse.fastenhealth.com/sandbox'
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
//specify the lighthouse server that we're going to use to authenticate against all our source/providers. Must not have trailing slash
lighthouse_api_endpoint_base: 'https://lighthouse.fastenhealth.com/sandbox',
//used to specify the couchdb server that we're going to use (can be relative or absolute). Must not have trailing slash
couchdb_endpoint_base: 'https://couchdb.sandbox.fastenhealth.com/database',
// if relative, must start with /
// couchdb_endpoint_base: '/database'
//used to specify the api server that we're going to use (can be relative or absolute). Must not have trailing slash
fasten_api_endpoint_base: 'https://api.sandbox.fastenhealth.com/v1',
// if relative, must start with /
// fasten_api_endpoint_base: '/api'
};

View File

@ -23,7 +23,19 @@
rel="stylesheet"
/>
<script>
var baseHref = window.location.pathname.split('/web').slice(0, 1)[0] + '/web/'
var baseHref = "/"
// if the pathname includes /web, everthing before `/web` (and including web) should be set as the base path.
if(window.location.pathname.includes('/web')){
baseHref = "/web/"
// probably running locally, and *may* include a subpath
var subPath = window.location.pathname.split('/web').slice(0, 1)[0]
if(subPath != "/"){
//subpath, so we need to update the absolutePath with the subpath before adding the relative path to the end
baseHref = subPath + '/web/'
}
}
document.write(`<base href="${baseHref}"/>`); </script>
</head>
<body>

View File

@ -146,6 +146,7 @@ export abstract class BaseClient {
}
private getCORSProxyPath(): string {
//TODO: this path should be passed in as a variable
const basePath = globalThis.location.pathname.split('/web').slice(0, 1)[0];
return `${globalThis.location.origin}${basePath || '/'}cors/`

View File

@ -70,7 +70,7 @@ describe('FHIR401Client', () => {
let current_user = uuidv4()
let cryptoConfig = await PouchdbCrypto.CryptConfig(current_user, current_user)
await PouchdbCrypto.StoreCryptConfig(cryptoConfig)
repository = NewPouchdbRepositoryWebWorker(current_user, new PouchDB("FHIR401Client-"+ current_user));
repository = NewPouchdbRepositoryWebWorker(current_user, '/database', new PouchDB("FHIR401Client-"+ current_user));
});
afterEach(async () => {

View File

@ -46,4 +46,6 @@ export interface IDatabaseRepository {
GetResource(resource_id: string): Promise<ResourceFhir>
GetResources(): Promise<IDatabasePaginatedResponse>
GetResourcesForSource(source_id: string, source_resource_type?: string): Promise<IDatabasePaginatedResponse>
GetEndpointAbsolutePath(currentUrl: {pathname: string, protocol: string, host: string}, relativePath: string): string
}

View File

@ -14,7 +14,7 @@ describe('PouchdbRepository', () => {
let current_user = uuidv4()
let cryptoConfig = await PouchdbCrypto.CryptConfig(current_user, current_user)
await PouchdbCrypto.StoreCryptConfig(cryptoConfig)
repository = NewPouchdbRepositoryWebWorker(current_user, new PouchDB("PouchdbRepository" + current_user));
repository = NewPouchdbRepositoryWebWorker(current_user, '/database', new PouchDB("PouchdbRepository" + current_user));
});
afterEach(async () => {
@ -98,4 +98,42 @@ describe('PouchdbRepository', () => {
expect((sourcesWrapped.rows[0] as Source).patient).toEqual('patient1');
});
})
describe('GetEndpointAbsolutePath', () => {
describe('with no subpath and no /web/', () => {
it('should return absolute path', async () => {
let currentUrl = new URL("http://www.example.com/")
const absoluteUrl = repository.GetEndpointAbsolutePath(currentUrl, '/database')
expect(absoluteUrl).toEqual('http://www.example.com/database');
});
})
describe('with subpath and no /web/', () => {
it('should return absolute path', async () => {
let currentUrl = new URL("http://www.example.com/hello/world")
const absoluteUrl = repository.GetEndpointAbsolutePath(currentUrl, '/database')
expect(absoluteUrl).toEqual('http://www.example.com/database');
});
})
describe('with no subpath and /web/', () => {
it('should return absolute path', async () => {
let currentUrl = new URL("http://www.example.com/web/world")
const absoluteUrl = repository.GetEndpointAbsolutePath(currentUrl, '/database')
expect(absoluteUrl).toEqual('http://www.example.com/database');
});
})
describe('with subpath and /web/', () => {
it('should return absolute path', async () => {
let currentUrl = new URL("http://www.example.com/fasten/web/hello/world")
const absoluteUrl = repository.GetEndpointAbsolutePath(currentUrl, '/database')
expect(absoluteUrl).toEqual('http://www.example.com/fasten/database');
});
})
})
});

View File

@ -49,8 +49,8 @@ import {PouchdbCryptConfig, PouchdbCrypto, PouchdbCryptoOptions} from './plugins
* @constructor
*/
export function NewPouchdbRepositoryWebWorker(current_user: string, localPouchDb?: PouchDB.Database): PouchdbRepository {
let pouchdbRepository = new PouchdbRepository(localPouchDb)
export function NewPouchdbRepositoryWebWorker(current_user: string, couchDbEndpointBase: string, localPouchDb?: PouchDB.Database): PouchdbRepository {
let pouchdbRepository = new PouchdbRepository(couchDbEndpointBase, localPouchDb)
pouchdbRepository.current_user = current_user
return pouchdbRepository
}
@ -72,8 +72,16 @@ export class PouchdbRepository implements IDatabaseRepository {
* @param userIdentifier
* @param encryptionKey
*/
constructor(localPouchDb?: PouchDB.Database) {
this.remotePouchEndpoint = `${globalThis.location.protocol}//${globalThis.location.host}${this.getBasePath()}/database`
constructor(couchDbEndpointBase: string, localPouchDb?: PouchDB.Database) {
// couchDbEndpointBase could be a relative or absolute path.
//if its absolute, we should pass it in, as-is
if (couchDbEndpointBase.indexOf('http://') === 0 || couchDbEndpointBase.indexOf('https://') === 0){
//absolute
this.remotePouchEndpoint = couchDbEndpointBase
} else {
//relative, we need to retrive the absolutePath from base
this.remotePouchEndpoint = this.GetEndpointAbsolutePath(globalThis.location, couchDbEndpointBase)
}
//setup PouchDB Plugins
//https://pouchdb.com/guides/mango-queries.html
@ -442,7 +450,21 @@ export class PouchdbRepository implements IDatabaseRepository {
///////////////////////////////////////////////////////////////////////////////////////
// Helper methods
///////////////////////////////////////////////////////////////////////////////////////
protected getBasePath(): string {
return globalThis.location.pathname.split('/web').slice(0, 1)[0];
//Fasten may be served behind a reverse proxy with a subpath, so lets try to find that component if it exists.
// if no subpath is found, just use the current url information to generate a path
public GetEndpointAbsolutePath(currentUrl: {pathname: string, protocol: string, host: string}, relativePath: string): string {
//no `/web` path to strip out, lets just use the relative path
let absolutePath = relativePath
if(currentUrl.pathname.includes('/web')){
// probably running locally, and *may* include a subpath
let subPath = currentUrl.pathname.split('/web').slice(0, 1)[0]
if(subPath != "/"){
//subpath, so we need to update the absolutePath with the subpath before adding the relative path to the end
absolutePath = subPath + relativePath
}
}
return `${currentUrl.protocol}//${currentUrl.host}${absolutePath}`
}
}