Merge pull request #8 from fastenhealth/encryption

This commit is contained in:
Jason Kulatunga 2022-10-17 22:24:37 -07:00 committed by GitHub
commit 53c0c66ceb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1063 additions and 81 deletions

View File

@ -142,7 +142,7 @@ The Fasten source code is organized into a handful of important folders, which w
│   │   │   │   └── source-detail # 2 column page displaying FHIR counts, and table listing FHIR resources for selected type
│   │   │   ├── services
│   │   │   │   ├── auth-interceptor.service.ts # service that looks for 401/403 API responses and triggers Logout
│   │   │   │   ├── can-activate.auth-guard.ts # service that checks if user is logged in
│   │   │   │   ├── is-authenticated-auth-guard.ts # service that checks if user is logged in
│   │   │   │   ├── fasten-api.service.ts # api service, used to commnunicate with Go backend (WILL BE REMOVED)
│   │   │   │   ├── fasten-db.service.ts # db service, used to communicate with CouchDB database
│   │   │   │   ├── lighthouse.service.ts # api service, used to communicate with auth-gateway (Lighthouse)

View File

@ -33,7 +33,9 @@
"chart.js": "2.9.4",
"crypto-pouch": "^4.0.1",
"fhirclient": "^2.5.1",
"garbados-crypt": "^3.0.0-beta",
"humanize-duration": "^3.27.3",
"idb": "^7.1.0",
"moment": "^2.29.4",
"ng2-charts": "^2.3.0",
"ngx-dropzone": "^3.1.0",
@ -45,6 +47,7 @@
"pouchdb-find": "^7.3.0",
"pouchdb-upsert": "^2.2.0",
"rxjs": "~6.5.4",
"transform-pouch": "^2.0.0",
"tslib": "^2.0.0",
"uuid": "^9.0.0",
"zone.js": "~0.11.8"

View File

@ -7,8 +7,10 @@ import { MedicalSourcesComponent } from './pages/medical-sources/medical-sources
import {ResourceDetailComponent} from './pages/resource-detail/resource-detail.component';
import {AuthSigninComponent} from './pages/auth-signin/auth-signin.component';
import {AuthSignupComponent} from './pages/auth-signup/auth-signup.component';
import {CanActivateAuthGuard} from './services/can-activate.auth-guard';
import {IsAuthenticatedAuthGuard} from './auth-guards/is-authenticated-auth-guard';
import {EncryptionEnabledAuthGuard} from './auth-guards/encryption-enabled.auth-guard';
import {SourceDetailComponent} from './pages/source-detail/source-detail.component';
import {EncryptionManagerComponent} from './pages/encryption-manager/encryption-manager.component';
const routes: Routes = [
@ -16,12 +18,13 @@ const routes: Routes = [
{ path: 'auth/signup', component: AuthSignupComponent },
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent, canActivate: [ CanActivateAuthGuard] },
{ path: 'source/:source_id', component: SourceDetailComponent, canActivate: [ CanActivateAuthGuard] },
{ path: 'resource/:resource_id', component: ResourceDetailComponent, canActivate: [ CanActivateAuthGuard] },
{ path: 'sources', component: MedicalSourcesComponent, canActivate: [ CanActivateAuthGuard] },
{ path: 'sources/callback/:source_type', component: MedicalSourcesComponent, canActivate: [ CanActivateAuthGuard] },
{ path: 'dashboard', component: DashboardComponent, canActivate: [ IsAuthenticatedAuthGuard, EncryptionEnabledAuthGuard] },
{ path: 'source/:source_id', component: SourceDetailComponent, canActivate: [ IsAuthenticatedAuthGuard, EncryptionEnabledAuthGuard] },
{ path: 'resource/:resource_id', component: ResourceDetailComponent, canActivate: [ IsAuthenticatedAuthGuard, EncryptionEnabledAuthGuard] },
{ path: 'sources', component: MedicalSourcesComponent, canActivate: [ IsAuthenticatedAuthGuard, EncryptionEnabledAuthGuard] },
{ path: 'sources/callback/:source_type', component: MedicalSourcesComponent, canActivate: [ IsAuthenticatedAuthGuard, EncryptionEnabledAuthGuard] },
{ path: 'account/security/manager', component: EncryptionManagerComponent, canActivate: [ IsAuthenticatedAuthGuard] },
// { path: 'general-pages', loadChildren: () => import('./general-pages/general-pages.module').then(m => m.GeneralPagesModule) },

View File

@ -1,12 +1,14 @@
import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
import {HttpClientTestingModule} from '@angular/common/http/testing';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule
RouterTestingModule,
HttpClientTestingModule
],
declarations: [
AppComponent

View File

@ -18,13 +18,15 @@ import { AuthSignupComponent } from './pages/auth-signup/auth-signup.component';
import { AuthSigninComponent } from './pages/auth-signin/auth-signin.component';
import { FormsModule } from '@angular/forms';
import { NgxDropzoneModule } from 'ngx-dropzone';
import { CanActivateAuthGuard } from './services/can-activate.auth-guard';
import { IsAuthenticatedAuthGuard } from './auth-guards/is-authenticated-auth-guard';
import { EncryptionEnabledAuthGuard } from './auth-guards/encryption-enabled.auth-guard';
import {FastenDbService} from './services/fasten-db.service';
import {Router} from '@angular/router';
import { SourceDetailComponent } from './pages/source-detail/source-detail.component';
import { HighlightModule, HIGHLIGHT_OPTIONS } from 'ngx-highlightjs';
import {AuthInterceptorService} from './services/auth-interceptor.service';
import { MomentModule } from 'ngx-moment';
import { EncryptionManagerComponent } from './pages/encryption-manager/encryption-manager.component';
@NgModule({
declarations: [
@ -37,6 +39,7 @@ import { MomentModule } from 'ngx-moment';
AuthSignupComponent,
AuthSigninComponent,
SourceDetailComponent,
EncryptionManagerComponent,
],
imports: [
FormsModule,
@ -58,7 +61,8 @@ import { MomentModule } from 'ngx-moment';
multi: true,
deps: [FastenDbService, Router]
},
CanActivateAuthGuard,
IsAuthenticatedAuthGuard,
EncryptionEnabledAuthGuard,
{
provide: HIGHLIGHT_OPTIONS,
useValue: {

View File

@ -0,0 +1,19 @@
import { Injectable } from '@angular/core';
import {CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router} from '@angular/router';
import {FastenDbService} from '../services/fasten-db.service';
@Injectable()
export class EncryptionEnabledAuthGuard implements CanActivate {
constructor(private fastenDbService: FastenDbService, private router: Router) {
}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise <boolean> {
//check if the user has encryption data stored in this browser already
if (!await this.fastenDbService.isCryptConfigAvailable()) {
return await this.router.navigate(['/account/security/manager']);
}
// continue as normal
return true
}
}

View File

@ -1,9 +1,9 @@
import { Injectable } from '@angular/core';
import {CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router} from '@angular/router';
import { FastenDbService } from './fasten-db.service';
import { FastenDbService } from '../services/fasten-db.service';
@Injectable()
export class CanActivateAuthGuard implements CanActivate {
export class IsAuthenticatedAuthGuard implements CanActivate {
constructor(private fastenDbService: FastenDbService, private router: Router) {
}
@ -11,7 +11,7 @@ export class CanActivateAuthGuard implements CanActivate {
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise <boolean> {
//check if the user is authenticated, if not, redirect to login
if (! await this.fastenDbService.IsAuthenticated()) {
this.router.navigate(['/auth/signin']);
return await this.router.navigate(['/auth/signin']);
}
// continue as normal
return true

View File

@ -61,7 +61,7 @@
<div class="az-img-user">
<img src="assets/logo/logo-text.png" alt="">
</div><!-- az-img-user -->
<h6>John Doe</h6>
<h6>{{current_user}}</h6>
<span>Adminstrator</span>
</div><!-- az-header-profile -->

View File

@ -7,10 +7,11 @@ import { Router } from '@angular/router';
styleUrls: ['./header.component.scss']
})
export class HeaderComponent implements OnInit {
current_user: string
constructor(private fastenDb: FastenDbService, private router: Router) { }
ngOnInit() {
this.current_user = this.fastenDb.current_user
}
closeMenu(e) {

View File

@ -2,8 +2,7 @@ import {Source} from '../../../lib/models/database/source';
export class SourceSyncMessage {
source: Source
userIdentifier: string
encryptionKey?: string
current_user: string
response?: any
}

View File

@ -0,0 +1,151 @@
<div class="az-content az-content-profile">
<div class="container">
<div class="row w-100">
<div class="col-sm-12">
<h2 class="az-content-title mg-t-20">Security Manager</h2>
<p class="mb-5">
Before you use Fasten you'll need to import or generate a new encryption key.
<br/>
<br/>
Fasten uses <a href="https://en.wikipedia.org/wiki/Zero-knowledge_service" target="_blank">zero-knowledge encryption</a> to secure your medical data.
This means that your medical records are encrypted on your device, before they are stored in the Fasten database.
The encrypted data stored in the database is worthless without your encryption key.
<br/>
<br/>
You must safely store your encryption key as you would a username & password,
as it's the only thing that will allow you to access your records.</p>
<div *ngIf="cryptoPanel == CryptoPanelType.Generate" class="wizard clearfix">
<div class="steps clearfix">
<ul role="tablist">
<li role="tab" class="first" [ngClass]="{'current': currentStep == 1, 'disabled': currentStep != 1}">
<a aria-controls="wizard1-p-0">
<span class="number">1</span>
<span class="title">Download</span></a>
</li>
<li role="tab" class="last" [ngClass]="{'current': currentStep == 2, 'disabled': currentStep != 2}">
<a aria-controls="wizard1-p-2">
<span class="number">2</span>
<span class="title">Validate</span>
</a>
</li>
</ul>
</div>
<div class="content clearfix">
<ng-container [ngSwitch]="currentStep">
<ng-container *ngSwitchCase="1">
<h3 tabindex="-1" class="title current">Generate an encryption key</h3>
<section role="tabpanel" aria-labelledby="wizard1-h-0" class="body current" aria-hidden="false">
<p class="mg-b-0">
Fasten has generated an encryption key for you. You can use this encryption key to decode your medical records on this browser, and other devices.
<br/>
This is the only time the encryption key will be available to view, copy or download. We recommend downloading this key and storing the file in a secure location.
<br/>
You can reset your encryption key at any time, however any previously encrypted data will no longer be accessible.
</p>
<pre><code [highlight]="currentCryptoConfig | json"></code></pre>
<div class="row row-xs wd-xl-80p">
<div class="col-sm-6 col-md-3">
<a [href]="generateCryptoConfigUrl" [download]="generateCryptoConfigFilename" class="btn btn-warning btn-rounded btn-block">Download Encryption Key</a>
</div>
</div>
</section>
</ng-container>
<ng-container *ngSwitchCase="2">
<h3 tabindex="-1" class="title current">Validate your encryption key</h3>
<section role="tabpanel" aria-labelledby="wizard1-h-0" class="body current" aria-hidden="false">
<p class="mg-b-10">
Please select your encryption key (which you generated in the previous step) using the file input below.
<br/>
It'll be validated against your browser's encryption key to ensure fidelity.
</p>
<div class="row">
<div class="col-4">
<div class="custom-file">
<input type="file" class="custom-file-input" id="generateCustomFile" (change)="generateOpenFileHandler($event.target.files)" accept="application/json">
<label class="custom-file-label" for="generateCustomFile">Choose file</label>
<div *ngIf="generateCustomFileError" class="alert alert-danger">
{{generateCustomFileError}}
</div>
</div>
</div>
</div>
</section>
</ng-container>
</ng-container>
</div>
<div *ngIf="currentStep != lastStep" class="actions clearfix">
<ul role="menu" aria-label="Pagination">
<li class="disabled" aria-disabled="true"></li>
<li aria-disabled="false"><button class="btn btn-az-primary btn-block" (click)="nextHandler()" role="menuitem">Next</button></li>
</ul>
</div>
</div>
<div *ngIf="cryptoPanel == CryptoPanelType.Import" class="wizard clearfix">
<div class="steps clearfix">
<ul role="tablist">
<li role="tab" class="first" [ngClass]="{'current': currentStep == 1, 'disabled': currentStep != 1}">
<a aria-controls="wizard1-p-0">
<span class="number">1</span>
<span class="title">Import</span></a>
</li>
<li role="tab" class="last" [ngClass]="{'current': currentStep == 2, 'disabled': currentStep != 2}">
<a aria-controls="wizard1-p-2">
<span class="number">2</span>
<span class="title">Validate</span>
</a>
</li>
</ul>
</div>
<div class="content clearfix">
<ng-container [ngSwitch]="currentStep">
<ng-container *ngSwitchCase="1">
<h3 tabindex="-1" class="title current">Import existing encryption key</h3>
<section role="tabpanel" aria-labelledby="wizard1-h-0" class="body current" aria-hidden="false">
<p class="mg-b-10">
Fasten was unable to find your encryption key on this device, and has detected encrypted data in your database.
<br/>
You will need to provide your encryption key to access your health records.
</p>
<div class="row">
<div class="col-4">
<div class="custom-file">
<input type="file" class="custom-file-input" id="importCustomFile" (change)="importOpenFileHandler($event.target.files)" accept="application/json">
<label class="custom-file-label" for="importCustomFile">Choose file</label>
<div *ngIf="importCustomFileError" class="alert alert-danger">
{{importCustomFileError}}
</div>
</div>
</div>
</div>
</section>
</ng-container>
<ng-container *ngSwitchCase="2">
<h3 tabindex="-1" class="title current">Validate encryption key</h3>
<section role="tabpanel" aria-labelledby="wizard1-h-0" class="body current" aria-hidden="false">
<p class="mg-b-10">
Thank you for providing your encryption key. Fasten will attempt to decrypt your secured records with this key.
</p>
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
</section>
</ng-container>
</ng-container>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,26 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EncryptionManagerComponent } from './encryption-manager.component';
import {HttpClientTestingModule} from '@angular/common/http/testing';
describe('EncryptionManagerComponent', () => {
let component: EncryptionManagerComponent;
let fixture: ComponentFixture<EncryptionManagerComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ EncryptionManagerComponent ],
imports: [HttpClientTestingModule],
})
.compileComponents();
fixture = TestBed.createComponent(EncryptionManagerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,183 @@
import { Component, OnInit } from '@angular/core';
import { v4 as uuidv4 } from 'uuid';
import {PouchdbCryptConfig, PouchdbCrypto} from '../../../lib/database/plugins/crypto';
import {FastenDbService} from '../../services/fasten-db.service';
import {DomSanitizer, SafeResourceUrl} from '@angular/platform-browser';
import {Router} from '@angular/router';
import {ToastService} from '../../services/toast.service';
import {ToastNotification, ToastType} from '../../models/fasten/toast';
export enum CryptoPanelType {
Loading,
Import,
Generate,
}
@Component({
selector: 'app-encryption-manager',
templateUrl: './encryption-manager.component.html',
styleUrls: ['./encryption-manager.component.scss']
})
export class EncryptionManagerComponent implements OnInit {
CryptoPanelType = CryptoPanelType
cryptoPanel: CryptoPanelType = CryptoPanelType.Loading
currentCryptoConfig: PouchdbCryptConfig = null
generateCryptoConfigUrl: SafeResourceUrl = ""
generateCryptoConfigFilename: string = ""
generateCustomFileError: string = ""
importCustomFileError: string = ""
currentStep: number
lastStep: number
constructor(private fastenDbService: FastenDbService, private sanitizer: DomSanitizer, private router: Router, private toastService: ToastService) { }
ngOnInit(): void {
this.fastenDbService.IsDatabasePopulated()
.then((isPopulated) => {
if(isPopulated){
return this.showImportCryptoConfig()
} else {
return this.showGenerateCryptoConfig()
}
})
}
nextHandler() {
this.currentStep += 1
// if (!this.stepsService.isLastStep()) {
// this.stepsService.moveToNextStep();
// } else {
// this.onSubmit();
// }
}
/////////////////////////////////////////////////////////////////////////////////////////////////
// Generate Wizard Methods
/////////////////////////////////////////////////////////////////////////////////////////////////
async showGenerateCryptoConfig(): Promise<PouchdbCryptConfig> {
this.cryptoPanel = CryptoPanelType.Generate
this.currentStep = 1
this.lastStep = 2
if(!this.currentCryptoConfig){
this.currentCryptoConfig = await PouchdbCrypto.CryptConfig(uuidv4(), this.fastenDbService.current_user)
await PouchdbCrypto.StoreCryptConfig(this.currentCryptoConfig) //store in indexdb
//generate URL for downloading.
let currentCryptoConfigBlob = new Blob([JSON.stringify(this.currentCryptoConfig)], { type: 'application/json' });
this.generateCryptoConfigUrl = this.sanitizer.bypassSecurityTrustResourceUrl(window.URL.createObjectURL(currentCryptoConfigBlob));
this.generateCryptoConfigFilename = `fasten-${this.fastenDbService.current_user}.key.json`
}
return this.currentCryptoConfig
}
generateOpenFileHandler(fileList: FileList) {
this.generateCustomFileError = ""
let file = fileList[0];
this.readFileContent(file)
.then((content) => {
let parsedCryptoConfig = JSON.parse(content) as PouchdbCryptConfig
//check if the parsed encryption key matches the currently set encryption key
if(parsedCryptoConfig.key == this.currentCryptoConfig.key &&
parsedCryptoConfig.username == this.currentCryptoConfig.username &&
parsedCryptoConfig.config == this.currentCryptoConfig.config){
return true
} else {
//throw an error & notify user
this.generateCustomFileError = "Crypto configuration file does not match"
throw new Error(this.generateCustomFileError)
}
})
.then(() => {
const toastNotification = new ToastNotification()
toastNotification.type = ToastType.Success
toastNotification.message = "Successfully validated & stored encryption key."
toastNotification.autohide = true
this.toastService.show(toastNotification)
//redirect user to dashboard
return this.router.navigate(['/dashboard']);
})
.catch(console.error)
}
/////////////////////////////////////////////////////////////////////////////////////////////////
// Import Wizard Methods
/////////////////////////////////////////////////////////////////////////////////////////////////
async showImportCryptoConfig(): Promise<any> {
this.cryptoPanel = CryptoPanelType.Import
this.currentStep = 1
this.lastStep = 2
}
importOpenFileHandler(fileList: FileList) {
this.importCustomFileError = ""
let file = fileList[0];
this.readFileContent(file)
.then((content) => {
let cryptoConfig = JSON.parse(content) as PouchdbCryptConfig
if(cryptoConfig.key && cryptoConfig.config){
return PouchdbCrypto.StoreCryptConfig(cryptoConfig)
} else {
//throw an error & notify user
this.importCustomFileError = "Invalid crypto configuration file"
throw new Error(this.importCustomFileError)
}
})
.then(() => {
//go to step 2
this.currentStep = 2
//attempt to initialize pouchdb with specified crypto
this.fastenDbService.ResetDB()
return this.fastenDbService.GetSources()
})
.then(() => {
const toastNotification = new ToastNotification()
toastNotification.type = ToastType.Success
toastNotification.message = "Successfully validated & imported encryption key."
toastNotification.autohide = true
this.toastService.show(toastNotification)
return this.router.navigate(['/dashboard']);
})
.catch((err) => {
console.error(err)
//an error occurred while importing credential
const toastNotification = new ToastNotification()
toastNotification.type = ToastType.Error
toastNotification.message = "Provided encryption key does not match. Please try a different key"
toastNotification.autohide = false
this.toastService.show(toastNotification)
this.currentStep = 1
})
}
private readFileContent(file: File): Promise<string>{
return new Promise<string>((resolve, reject) => {
if (!file) {
resolve('');
}
const reader = new FileReader();
reader.onload = (e) => {
const text = reader.result.toString();
resolve(text);
};
reader.readAsText(file);
});
}
}

View File

@ -18,6 +18,7 @@ PouchDB.plugin(PouchUpsert);
import * as PouchCrypto from 'crypto-pouch';
PouchDB.plugin(PouchCrypto);
import PouchAuth from 'pouchdb-authentication'
import {PouchdbCrypto} from '../../lib/database/plugins/crypto';
PouchDB.plugin(PouchAuth);
@Injectable({
@ -25,11 +26,8 @@ PouchDB.plugin(PouchAuth);
})
export class FastenDbService extends PouchdbRepository {
constructor(private _httpClient: HttpClient) {
const userIdentifier = localStorage.getItem("current_user")
super(userIdentifier, "my-secret-encryption-key");
super();
}
@ -46,6 +44,7 @@ export class FastenDbService extends PouchdbRepository {
let remotePouchDb = new PouchDB(this.getRemoteUserDb(username), {skip_setup: true});
return await remotePouchDb.logIn(username, pass)
.then((loginResp)=>{
this.current_user = loginResp.name
return this.postLoginHook(loginResp.name, remotePouchDb)
})
.catch((err) => {
@ -73,37 +72,68 @@ export class FastenDbService extends PouchdbRepository {
await this.pouchDb.logOut()
}
await this.Close()
localStorage.removeItem("current_user")
}
/**
* Try to get PouchDB database using session information
* @constructor
*/
public override async GetSessionDB(): Promise<PouchDB.Database> {
if(this.pouchDb){
console.log("Session DB already exists..")
return this.pouchDb
}
//Since we dont have a pre-configured pouchDB already, see if we have an active session to the remote database.
let sessionDb = new PouchDB(this.getRemoteUserDb("placeholder"))
const session = await sessionDb.getSession()
console.log("Session found...", session)
const authUser = session?.userCtx?.name
if(authUser){
this.pouchDb = new PouchDB(this.getRemoteUserDb(authUser))
this.current_user = authUser
}
return this.pouchDb
}
//TODO: now that we've moved to remote-first database, we can refactor and simplify this function significantly.
public async IsAuthenticated(): Promise<boolean> {
if(!this.pouchDb){
console.warn("IsAuthenticated? ====> logout, no local database present")
//if no local database available, we're always "unauthenticated".
return false
}
if(!localStorage.getItem("current_user")){
console.warn("IsAuthenticated? ====> logout, no current_user set")
return false
}
try{
//if we have a local database, lets see if we have an active session to the remote database.
// const remotePouchDb = new PouchDB(this.getRemoteUserDb(localStorage.getItem("current_user")), {skip_setup: true});
const session = await this.pouchDb.getSession()
const authUser = session?.userCtx?.name
const isAuth = !!authUser
console.warn("IsAuthenticated? getSession() ====> ", isAuth)
if(!isAuth){
//lets see if we have an active session to the remote database.
await this.GetSessionDB()
if(!this.pouchDb){
console.warn("could not determine database from session info, logging out")
return false
}
//confirm that the logged in user matches the session user
return authUser == localStorage.getItem("current_user")
let session = await this.pouchDb.getSession()
let authUser = session?.userCtx?.name
let isAuth = !!authUser
console.warn("IsAuthenticated? getSession() ====> ", isAuth)
return isAuth;
} catch (e) {
return false
}
}
/**
* Is the crypto configuration for the authenticated user already available in the browser? Or do we need to import/generate new config.
*/
public async isCryptConfigAvailable(): Promise<boolean>{
try {
await this.GetSessionDB()
let cryptConfig = await PouchdbCrypto.RetrieveCryptConfig(this.current_user)
return !!cryptConfig
}catch(e){
return false
}
}
public Close(): Promise<void> {
return super.Close()
}
@ -170,7 +200,6 @@ export class FastenDbService extends PouchdbRepository {
* @constructor
*/
private async postLoginHook(userIdentifier: string, pouchDb: PouchDB.Database): Promise<void> {
localStorage.setItem("current_user", userIdentifier)
await this.Close();
this.pouchDb = pouchDb;

View File

@ -1,12 +1,15 @@
import { TestBed } from '@angular/core/testing';
import { QueueService } from './queue.service';
import {HttpClientTestingModule} from '@angular/common/http/testing';
describe('QueueService', () => {
let service: QueueService;
beforeEach(() => {
TestBed.configureTestingModule({});
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
});
service = TestBed.inject(QueueService);
});

View File

@ -5,20 +5,20 @@ import {Source} from '../../lib/models/database/source';
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';
@Injectable({
providedIn: 'root'
})
export class QueueService {
constructor(private toastService: ToastService) { }
constructor(private toastService: ToastService, private fastenDbService: FastenDbService) { }
runSourceSyncWorker(source: Source):Observable<string> {
if (typeof Worker !== 'undefined') {
const sourceSync = new SourceSyncMessage()
sourceSync.source = source
sourceSync.userIdentifier = localStorage.getItem("current_user")
sourceSync.encryptionKey = "my-secret-encryption-key"
sourceSync.current_user = this.fastenDbService.current_user
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

@ -4,7 +4,7 @@ import {DoWork, runWorker} from 'observable-webworker';
import {Observable} from 'rxjs';
import {mergeMap} from 'rxjs/operators';
import {SourceSyncMessage} from '../models/queue/source-sync-message';
import {NewRepositiory} from '../../lib/database/pouchdb_repository';
import {NewPouchdbRepositoryWebWorker, PouchdbRepository} from '../../lib/database/pouchdb_repository';
import {NewClient} from '../../lib/conduit/factory';
import {Source} from '../../lib/models/database/source';
@ -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 = NewRepositiory(sourceSyncMessage.userIdentifier, sourceSyncMessage.encryptionKey)
const db = NewPouchdbRepositoryWebWorker(sourceSyncMessage.current_user)
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,307 @@
/***** DEFAULT STYLE WIZARD *****/
.wizard {
border: 1px solid #e3e7ed;
background-color: #fff; }
.wizard > .steps {
padding: 20px; }
@media (min-width: 768px) {
.wizard > .steps {
padding: 25px; } }
@media (min-width: 992px) {
.wizard > .steps {
padding: 30px; } }
.wizard > .steps > ul {
padding: 0;
margin-bottom: 0;
display: flex; }
.wizard > .steps > ul li {
float: none;
display: block;
width: auto; }
.wizard > .steps > ul li .current-info {
display: none; }
.wizard > .steps > ul li .title {
margin-left: 5px;
white-space: nowrap;
transition: all 0.2s ease-in-out; }
@media (prefers-reduced-motion: reduce) {
.wizard > .steps > ul li .title {
transition: none; } }
@media (min-width: 576px) {
.wizard > .steps > ul li .title {
display: none;
margin-left: 10px; } }
@media (min-width: 768px) {
.wizard > .steps > ul li .title {
display: inline-block; } }
.wizard > .steps > ul li + li {
margin-left: 5px; }
@media (min-width: 576px) {
.wizard > .steps > ul li + li {
margin-left: 20px; } }
@media (min-width: 992px) {
.wizard > .steps > ul li + li {
margin-left: 30px; } }
.wizard > .steps a,
.wizard > .steps a:hover,
.wizard > .steps a:active {
color: #1c273c;
font-weight: 500;
font-size: 15px;
display: flex;
justify-content: center;
align-items: center; }
@media (min-width: 1200px) {
.wizard > .steps a,
.wizard > .steps a:hover,
.wizard > .steps a:active {
justify-content: flex-start; } }
.wizard > .steps a .number,
.wizard > .steps a:hover .number,
.wizard > .steps a:active .number {
flex-shrink: 0;
font-weight: 700;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
color: #fff;
display: block;
text-align: center;
line-height: 2;
width: 30px;
height: 30px;
background-color: #b4bdce;
border-radius: 100%;
transition: all 0.2s ease-in-out; }
@media (prefers-reduced-motion: reduce) {
.wizard > .steps a .number,
.wizard > .steps a:hover .number,
.wizard > .steps a:active .number {
transition: none; } }
@media (min-width: 576px) {
.wizard > .steps a .number,
.wizard > .steps a:hover .number,
.wizard > .steps a:active .number {
font-size: 18px;
font-weight: 600;
line-height: 2.1;
width: 40px;
height: 40px; } }
.wizard > .steps .disabled {
display: none; }
@media (min-width: 576px) {
.wizard > .steps .disabled {
display: block; } }
.wizard > .steps .disabled a,
.wizard > .steps .disabled a:hover,
.wizard > .steps .disabled a:active {
color: #97a3b9; }
.wizard > .steps .current a, .wizard > .steps .current a:hover, .wizard > .steps .current a:active {
color: #5b47fb; }
.wizard > .steps .current a .title, .wizard > .steps .current a:hover .title, .wizard > .steps .current a:active .title {
display: inline-block; }
.wizard > .steps .current a .number, .wizard > .steps .current a:hover .number, .wizard > .steps .current a:active .number {
background-color: #5b47fb; }
.wizard > .steps .done a, .wizard > .steps .done a:hover, .wizard > .steps .done a:active {
color: #00cccc; }
.wizard > .steps .done a .title, .wizard > .steps .done a:hover .title, .wizard > .steps .done a:active .title {
display: none; }
@media (min-width: 768px) {
.wizard > .steps .done a .title, .wizard > .steps .done a:hover .title, .wizard > .steps .done a:active .title {
display: inline-block; } }
.wizard > .steps .done a .number, .wizard > .steps .done a:hover .number, .wizard > .steps .done a:active .number {
background-color: #00cccc; }
.wizard > .content {
border-top: 1px solid #e3e7ed;
border-bottom: 1px solid #e3e7ed;
min-height: 150px;
padding: 20px; }
@media (min-width: 768px) {
.wizard > .content {
padding: 25px; } }
@media (min-width: 992px) {
.wizard > .content {
padding: 30px; } }
.wizard > .content > .title {
font-size: 18px;
color: #1c273c;
font-weight: 700;
margin-bottom: 5px;
display: none; }
.wizard > .content > .title.current {
display: block; }
.wizard > .content > .body {
float: none;
position: static;
width: auto;
height: auto; }
.wizard > .content > .body input.parsley-error {
border-color: #dc3545; }
.wizard > .content > .body input.parsley-error + ul {
list-style: none !important; }
.wizard > .actions {
padding: 20px; }
@media (min-width: 768px) {
.wizard > .actions {
padding: 25px; } }
@media (min-width: 992px) {
.wizard > .actions {
padding: 30px; } }
.wizard > .actions > ul {
margin: 0;
padding: 0;
list-style: none;
display: flex;
justify-content: space-between; }
.wizard > .actions > ul > li:last-child a {
background-color: #00cccc; }
.wizard > .actions a,
.wizard > .actions a:hover,
.wizard > .actions a:active {
display: block;
background-color: #5b47fb;
padding: 9px 25px;
line-height: 1.573;
color: #fff; }
.wizard > .actions .disabled a,
.wizard > .actions .disabled a:hover,
.wizard > .actions .disabled a:active {
background-color: #97a3b9; }
.wizard.vertical > .steps {
padding: 20px; }
@media (min-width: 576px) {
.wizard.vertical > .steps {
float: left;
width: 20%; } }
@media (min-width: 768px) {
.wizard.vertical > .steps {
width: 15%; } }
@media (min-width: 992px) {
.wizard.vertical > .steps {
padding: 25px;
width: 30%; } }
@media (min-width: 576px) {
.wizard.vertical > .steps ul {
flex-direction: column; } }
.wizard.vertical > .steps ul li + li {
margin-left: 10px; }
@media (min-width: 576px) {
.wizard.vertical > .steps ul li + li {
margin-top: 10px;
margin-left: 0; } }
@media (min-width: 768px) {
.wizard.vertical > .steps ul li + li {
margin-top: 20px; } }
.wizard.vertical > .steps ul li .title {
display: none; }
@media (min-width: 992px) {
.wizard.vertical > .steps ul li .title {
display: block; } }
@media (min-width: 992px) {
.wizard.vertical > .steps a {
justify-content: flex-start; } }
.wizard.vertical > .steps .current a .title {
display: inline-block; }
@media (min-width: 576px) {
.wizard.vertical > .steps .current a .title {
display: none; } }
@media (min-width: 992px) {
.wizard.vertical > .steps .current a .title {
display: inline-block; } }
.wizard.vertical > .content {
margin: 0;
padding: 20px; }
@media (min-width: 576px) {
.wizard.vertical > .content {
border-top-width: 0;
border-bottom-width: 0;
width: 80%;
float: right;
border-left: 1px solid #cdd4e0; } }
@media (min-width: 768px) {
.wizard.vertical > .content {
width: 85%; } }
@media (min-width: 992px) {
.wizard.vertical > .content {
width: 70%;
padding: 25px; } }
.wizard.vertical > .actions {
padding: 20px; }
@media (min-width: 576px) {
.wizard.vertical > .actions {
width: 80%;
float: right;
border-left: 1px solid #cdd4e0; } }
@media (min-width: 768px) {
.wizard.vertical > .actions {
width: 85%; } }
@media (min-width: 992px) {
.wizard.vertical > .actions {
width: 70%;
padding: 25px; } }
.wizard.vertical > .actions ul {
float: none;
margin: 0;
padding: 0; }
/****** EQUAL COLUMN WIDTH STEP INDICATOR *****/
.step-equal-width > .steps > ul {
display: flex; }
.step-equal-width > .steps > ul > li {
flex: 1;
width: auto;
float: none; }
/****** EQUAL COLUMN WIDTH STEP INDICATOR *****/
.step-equal-width > .steps > ul {
display: flex; }
.step-equal-width > .steps > ul > li {
flex: 1;
width: auto;
float: none; }
/***** CUSTOM STYLES *****/
.wizard-style-1 > .steps > ul a, .wizard-style-1 > .steps > ul a:hover, .wizard-style-1 > .steps > ul a:active {
padding: 0;
height: 50px; }
.wizard-style-1 > .steps > ul a .number, .wizard-style-1 > .steps > ul a:hover .number, .wizard-style-1 > .steps > ul a:active .number {
width: 50px;
height: 100%;
border: 0;
font-size: 18px;
font-weight: bold;
color: #7987a1;
background-color: #cdd4e0;
border-radius: 0; }
.wizard-style-1 > .steps > ul a .title, .wizard-style-1 > .steps > ul a:hover .title, .wizard-style-1 > .steps > ul a:active .title {
margin-right: 20px;
margin-left: 20px; }
.wizard-style-1 > .steps > ul .current a .number, .wizard-style-1 > .steps > ul .current a:hover .number, .wizard-style-1 > .steps > ul .current a:active .number {
background-color: #452efa;
color: #fff; }
.wizard-style-1 > .steps > ul .done a .number, .wizard-style-1 > .steps > ul .done a:hover .number, .wizard-style-1 > .steps > ul .done a:active .number {
background-color: #643ab0;
color: #fff; }
.wizard-style-2 > .steps > ul a, .wizard-style-2 > .steps > ul a:hover, .wizard-style-2 > .steps > ul a:active {
padding: 0;
height: 50px;
border-radius: 50px; }
.wizard-style-2 > .steps > ul a .number, .wizard-style-2 > .steps > ul a:hover .number, .wizard-style-2 > .steps > ul a:active .number {
width: 50px;
height: 100%;
border: 2px solid #e3e7ed;
font-size: 18px;
font-weight: bold;
color: #7987a1;
background-color: #fff; }
.wizard-style-2 > .steps > ul a .title, .wizard-style-2 > .steps > ul a:hover .title, .wizard-style-2 > .steps > ul a:active .title {
margin-right: 20px; }
.wizard-style-2 > .steps > ul .current a .number, .wizard-style-2 > .steps > ul .current a:hover .number, .wizard-style-2 > .steps > ul .current a:active .number {
border-color: #5b47fb;
color: #5b47fb; }
.wizard-style-2 > .steps > ul .done a .number, .wizard-style-2 > .steps > ul .done a:hover .number, .wizard-style-2 > .steps > ul .done a:active .number {
border-color: #6f42c1;
color: #6f42c1; }

View File

@ -2,7 +2,7 @@ import {FHIR401Client} from './fhir401_r4_client';
import {Source} from '../../../models/database/source';
import {IResourceBundleRaw} from '../../interface';
import {ResourceFhir} from '../../../models/database/resource_fhir';
import {NewRepositiory} from '../../../database/pouchdb_repository';
import {NewPouchdbRepositoryWebWorker} from '../../../database/pouchdb_repository';
import {Base64} from '../../../utils/base64';
import * as PouchDB from 'pouchdb/dist/pouchdb';
import { v4 as uuidv4 } from 'uuid';
@ -10,6 +10,7 @@ import { v4 as uuidv4 } from 'uuid';
// @ts-ignore
import * as FHIR401Client_ProcessBundle from './fixtures/FHIR401Client_ProcessBundle.json';
import {IDatabaseRepository} from '../../../database/interface';
import {PouchdbCrypto} from '../../../database/plugins/crypto';
class TestClient extends FHIR401Client {
@ -66,7 +67,10 @@ describe('FHIR401Client', () => {
let repository: IDatabaseRepository;
beforeEach(async () => {
repository = NewRepositiory(null, null, new PouchDB("FHIR401Client-"+ uuidv4()));
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));
});
afterEach(async () => {

View File

@ -22,7 +22,7 @@ export interface IDatabasePaginatedResponse {
rows: any[]
}
export interface IDatabaseRepository {
GetDB(): any
GetDB(skipEncryption?: boolean): any
Close(): void
// CreateUser(*models.User) error
@ -34,7 +34,7 @@ export interface IDatabaseRepository {
DeleteSource(source_id: string): Promise<boolean>
GetSourceSummary(source_id: string): Promise<SourceSummary>
GetSources(): Promise<IDatabasePaginatedResponse>
IsDatabasePopulated(): Promise<boolean>
// UpsertResource(context.Context, *models.ResourceFhir) error
// GetResourceBySourceType(context.Context, string, string) (*models.ResourceFhir, error)

View File

@ -0,0 +1,151 @@
// This is a Typescript module that recreates the functionality defined in https://github.com/calvinmetcalf/crypto-pouch/blob/master/index.js
// This file only exists because the PouchDB crypto plugin must work in both the browser and web-worker environment (where `window` is
// undefined and causes errors).
// Also, crypto-pouch does not support storing encrypted data in the remote database by default, which I'm attempting to do by commenting out the
// NO_COUCH error.
//
// We've attempted to use the Typescript Module Plugin/Augmentation pattern to modify the global `pouchdb` object, however that
// failed for a variety of reasons, so instead we're using a PouchdbCrypto class with static methods to re-implement the crypto logic
//
//
// See:
// - https://github.com/calvinmetcalf/crypto-pouch/blob/master/index.js
// - https://www.typescriptlang.org/docs/handbook/declaration-files/templates/module-plugin-d-ts.html
// - https://www.typescriptlang.org/docs/handbook/declaration-merging.html
// - https://www.typescriptlang.org/docs/handbook/declaration-files/templates/global-plugin-d-ts.html
// - https://www.typescriptlang.org/docs/handbook/declaration-files/templates/global-modifying-module-d-ts.html
// - https://stackoverflow.com/questions/35074713/extending-typescript-global-object-in-node-js
// - https://github.com/Microsoft/TypeScript/issues/15818
import * as Crypt from 'garbados-crypt';
import { openDB, deleteDB, wrap, unwrap } from 'idb';
// const Crypt = require()
const LOCAL_ID = '_local/crypto'
const IGNORE = ['_id', '_rev', '_deleted', '_conflicts']
// const NO_COUCH = 'crypto-pouch does not work with pouchdb\'s http adapter. Use a local adapter instead.'
export class PouchdbCryptoOptions {
ignore?: string[]
}
export class PouchdbCryptConfig {
username: string
key: string
config: string
}
export class PouchdbCrypto {
private static async localIdb(){
const dbPromise = openDB('crypto-store', 1, {
upgrade(db) {
db.createObjectStore('crypto');
},
});
return await dbPromise
}
public static async CryptConfig(key: string, username): Promise<PouchdbCryptConfig>{
const _crypt = new Crypt(key)
let exportString = await _crypt.export()
return {username: username, key: key, config: exportString}
}
public static async StoreCryptConfig(cryptConfig: PouchdbCryptConfig) {
const localDb = await PouchdbCrypto.localIdb()
await localDb.put('crypto', JSON.stringify(cryptConfig), `encryption_data_${cryptConfig.username}`)
}
public static async RetrieveCryptConfig(currentUser: string): Promise<PouchdbCryptConfig>{
const localDb = await PouchdbCrypto.localIdb()
let cryptConfigStr = await localDb.get('crypto',`encryption_data_${currentUser}`)
if(!cryptConfigStr){
throw new Error("crypto configuration not set")
}
return JSON.parse(cryptConfigStr) as PouchdbCryptConfig
}
public static async crypto(db, cryptConfig: PouchdbCryptConfig, options: PouchdbCryptoOptions = {}) {
// if (db.adapter === 'http') {
// throw new Error(NO_COUCH)
// }
// if (typeof password === 'object') {
// // handle `db.crypto({ password, ...options })`
// options = password
// password = password.password
// delete options.password
// }
// setup ignore list
db._ignore = IGNORE.concat(options.ignore || [])
if(!cryptConfig || !cryptConfig.key || !cryptConfig.config){
throw new Error("crypto configuration file is required")
}
// setup crypto helper
const trySetup = async () => {
// try saving credentials to a local doc
try {
// // first we try to get saved creds from the local doc
// const localDb = await PouchdbCrypto.localIdb()
// let exportString = await localDb.get('crypto',`encryption_data_${db.name}`)
// if(!exportString){
// // no existing encryption key found
//
// // do first-time setup
// db._crypt = new Crypt(password)
// let exportString = await db._crypt.export()
// await localDb.put('crypto', exportString, `encryption_data_${db.name}`)
// } else {
//
// }
db._crypt = await Crypt.import(cryptConfig.key, cryptConfig.config)
} catch (err) {
throw err
}
}
await trySetup()
// instrument document transforms
db.transform({
incoming: async (doc) => {
// if no crypt, ex: after .removeCrypto(), just return the doc
if (!db._crypt) {
return doc
}
if (doc._attachments && !db._ignore.includes('_attachments')) {
throw new Error('Attachments cannot be encrypted. Use {ignore: "_attachments"} option')
}
let encrypted: any = {}
for (let key of db._ignore) {
// attach ignored fields to encrypted doc
if (key in doc) encrypted[key] = doc[key]
}
encrypted.payload = await db._crypt.encrypt(JSON.stringify(doc))
return encrypted
},
outgoing: async (doc) => {
// if no crypt, ex: after .removeCrypto(), just return the doc
if (!db._crypt) { return doc }
let decryptedString = await db._crypt.decrypt(doc.payload)
let decrypted = JSON.parse(decryptedString)
for (let key of db._ignore) {
// patch decrypted doc with ignored fields
if (key in doc) decrypted[key] = doc[key]
}
return decrypted
}
})
return db
}
public static removeCrypto(db) {
delete db._crypt
}
}

View File

@ -1,3 +1,18 @@
// This is a Typescript module that recreates the functionality defined in https://github.com/pouchdb/upsert/blob/master/index.js
// This file only exists because the PouchDB upsert plugin must work in both the browser and web-worker environment (where `window` is
// undefined and causes errors).
//
// We've attempted to use the Typescript Module Plugin/Augmentation pattern to modify the global `pouchdb` object, however that
// failed for a variety of reasons, so instead we're using a PouchdbUpsert class with static methods to re-implement the upsert logic
//
// See:
// - https://github.com/pouchdb/upsert/blob/master/index.js
// - https://www.typescriptlang.org/docs/handbook/declaration-files/templates/module-plugin-d-ts.html
// - https://www.typescriptlang.org/docs/handbook/declaration-merging.html
// - https://www.typescriptlang.org/docs/handbook/declaration-files/templates/global-plugin-d-ts.html
// - https://www.typescriptlang.org/docs/handbook/declaration-files/templates/global-modifying-module-d-ts.html
// - https://stackoverflow.com/questions/35074713/extending-typescript-global-object-in-node-js
// - https://github.com/Microsoft/TypeScript/issues/15818
export class PouchdbUpsert {
public static upsert(db, docId, diffFun, cb?) {
var promise = PouchdbUpsert.upsertInner(db, docId, diffFun);

View File

@ -1,16 +1,20 @@
import {IDatabaseRepository} from './interface';
import {NewRepositiory} from './pouchdb_repository';
import {NewPouchdbRepositoryWebWorker} from './pouchdb_repository';
import {SourceType} from '../models/database/source_types';
import {Source} from '../models/database/source';
import {DocType} from './constants';
import * as PouchDB from 'pouchdb/dist/pouchdb';
import { v4 as uuidv4 } from 'uuid';
import {PouchdbCrypto} from './plugins/crypto';
describe('PouchdbRepository', () => {
let repository: IDatabaseRepository;
beforeEach(async () => {
repository = NewRepositiory(null, null, new PouchDB("PouchdbRepository" + uuidv4()));
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));
});
afterEach(async () => {

View File

@ -7,12 +7,17 @@ import {Base64} from '../utils/base64';
// PouchDB & plugins
import * as PouchDB from 'pouchdb/dist/pouchdb';
import * as PouchCrypto from 'crypto-pouch';
// import * as PouchCrypto from 'crypto-pouch';
// PouchDB.plugin(PouchCrypto);
import * as PouchTransform from 'transform-pouch';
PouchDB.plugin(PouchTransform);
import {PouchdbUpsert} from './plugins/upsert';
import {UpsertSummary} from '../models/fasten/upsert-summary';
PouchDB.plugin(PouchCrypto);
import {PouchdbCryptConfig, PouchdbCrypto, PouchdbCryptoOptions} from './plugins/crypto';
// !!!!!!!!!!!!!!!!WARNING!!!!!!!!!!!!!!!!!!!!!
// most pouchdb plugins seem to fail when used in a webworker.
@ -43,16 +48,22 @@ PouchDB.plugin(PouchCrypto);
* Eventually this method should dyanmically dtermine the version of the repo to return from the env.
* @constructor
*/
export function NewRepositiory(userIdentifier?: string, encryptionKey?: string, localPouchDb?: PouchDB.Database): IDatabaseRepository {
return new PouchdbRepository(userIdentifier, encryptionKey, localPouchDb)
}
export function NewPouchdbRepositoryWebWorker(current_user: string, localPouchDb?: PouchDB.Database): PouchdbRepository {
let pouchdbRepository = new PouchdbRepository(localPouchDb)
pouchdbRepository.current_user = current_user
return pouchdbRepository
}
export class PouchdbRepository implements IDatabaseRepository {
// replicationHandler: any
remotePouchEndpoint: string // "http://localhost:5984"
encryptionKey: string
pouchDb: PouchDB.Database
current_user: string
//encryption configuration
cryptConfig: PouchdbCryptConfig = null
encryptionInitComplete: boolean = false
/**
* This class can be initialized in 2 states
@ -61,17 +72,13 @@ export class PouchdbRepository implements IDatabaseRepository {
* @param userIdentifier
* @param encryptionKey
*/
constructor(userIdentifier?: string, encryptionKey?: string, localPouchDb?: PouchDB.Database) {
constructor(localPouchDb?: PouchDB.Database) {
this.remotePouchEndpoint = `${globalThis.location.protocol}//${globalThis.location.host}${this.getBasePath()}/database`
//setup PouchDB Plugins
//https://pouchdb.com/guides/mango-queries.html
this.pouchDb = null
if(userIdentifier){
this.pouchDb = new PouchDB(this.getRemoteUserDb(userIdentifier));
this.encryptionKey = encryptionKey
// this.enableSync(userIdentifier)
}
if(localPouchDb){
console.warn("using local pouchdb, this should only be used for testing")
this.pouchDb = localPouchDb
@ -203,6 +210,30 @@ export class PouchdbRepository implements IDatabaseRepository {
})
}
/**
* given an raw connection to a database, determine how many records/resources are stored within
* @constructor
*/
public async IsDatabasePopulated(): Promise<boolean> {
let resourceFhirCount = await this.findDocumentByPrefix(DocType.ResourceFhir, false, true)
.then((resp) => {
console.log("RESPONSE COUNT INFO", resp)
return resp.rows.length
})
if(resourceFhirCount > 0) {return true}
let sourceCount = await this.findDocumentByPrefix(DocType.Source, false, true)
.then((resp) => {
console.log("SOURCE COUNT INFO", resp)
return resp.rows.length
})
if(sourceCount > 0) {return true}
return false
}
///////////////////////////////////////////////////////////////////////////////////////
@ -210,25 +241,48 @@ export class PouchdbRepository implements IDatabaseRepository {
// All functions below here will return the raw PouchDB responses, and may need to be wrapped in
// new ResourceFhir(result.doc)
///////////////////////////////////////////////////////////////////////////////////////
public ResetDB(){
this.pouchDb = null
this.encryptionInitComplete = false
}
// Get the active PouchDB instance. Throws an error if no PouchDB instance is
// available (ie, user has not yet been configured with call to .configureForUser()).
public async GetDB(): Promise<PouchDB.Database> {
public async GetDB(skipEncryption: boolean = false): Promise<PouchDB.Database> {
await this.GetSessionDB()
if(!this.pouchDb) {
throw(new Error( "Database is not available - please configure an instance." ));
}
// if(this.encryptionKey){
// return this.pouchDb.crypto(this.encryptionKey, {ignore:[
// 'doc_type',
// 'source_id',
// 'source_resource_type',
// 'source_resource_id',
// ]}).then(() => {
// return this.pouchDb
// })
// } else {
if(skipEncryption){
//this allows the database to be queried, even when encryption has not been configured correctly
//this will only be used to take a count of documents in the database, so we can prompt the user for a encryption key, or generate a new one (for an empty db)
return this.pouchDb
}
//try to determine the crypto configuration using the currently logged in user.
this.cryptConfig = await PouchdbCrypto.RetrieveCryptConfig(this.current_user)
if(!this.cryptConfig){
throw new Error("crypto configuration not set.")
}
if(!this.encryptionInitComplete){
return PouchdbCrypto.crypto(this.pouchDb, this.cryptConfig, {ignore:[
'doc_type',
'source_id',
'source_resource_type',
'source_resource_id',
]})
.then((encryptedPouchDb) => {
this.pouchDb = encryptedPouchDb
this.encryptionInitComplete = true
return this.pouchDb
})
} else {
return this.pouchDb;
// }
}
}
@ -324,8 +378,8 @@ export class PouchdbRepository implements IDatabaseRepository {
protected findDocumentByDocType(docType: DocType, includeDocs: boolean = true): Promise<IDatabasePaginatedResponse> {
return this.findDocumentByPrefix(docType, includeDocs)
}
protected findDocumentByPrefix(prefix: string, includeDocs: boolean = true): Promise<IDatabasePaginatedResponse> {
return this.GetDB()
protected findDocumentByPrefix(prefix: string, includeDocs: boolean = true, skipEncryption: boolean = false): Promise<IDatabasePaginatedResponse> {
return this.GetDB(skipEncryption)
.then((db) => {
return db.allDocs({
include_docs: includeDocs,
@ -348,6 +402,24 @@ export class PouchdbRepository implements IDatabaseRepository {
///////////////////////////////////////////////////////////////////////////////////////
// Sync private/protected methods
///////////////////////////////////////////////////////////////////////////////////////
/**
* Try to get PouchDB database using session information
* This method is overridden in PouchDB Service, as session information is inaccessible in web-worker
* @constructor
*/
public async GetSessionDB(): Promise<PouchDB.Database> {
if(this.pouchDb){
console.log("Session DB already exists..")
return this.pouchDb
}
if(!this.current_user){
throw new Error("current user is required when initializing pouchdb within web-worker")
}
this.pouchDb = new PouchDB(this.getRemoteUserDb(this.current_user))
return this.pouchDb
}
protected getRemoteUserDb(username: string){
return `${this.remotePouchEndpoint}/userdb-${this.toHex(username)}`
}

View File

@ -184,6 +184,7 @@
@import "./assets/scss/custom/list";
@import "./assets/scss/custom/nav";
@import "./assets/scss/custom/modal";
@import "./assets/scss/custom/wizard";
/* ############### CUSTOM VENDOR STYLES ############### */
@import "./assets/scss/lib/select2";

View File

@ -4579,6 +4579,11 @@ icss-utils@^5.0.0, icss-utils@^5.1.0:
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
idb@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.0.tgz#2cc886be57738419e57f9aab58f647e5e2160270"
integrity sha512-Wsk07aAxDsntgYJY4h0knZJuTxM73eQ4reRAO+Z1liOh8eMCJ/MoDS8fCui1vGT9mnjtl1sOu3I2i/W1swPYZg==
ieee754@^1.1.13:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"