Merge pull request #6 from fastenhealth/sandbox
This commit is contained in:
commit
94d26ad075
|
@ -10,7 +10,7 @@ env:
|
|||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
omnibus:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
@ -29,10 +29,10 @@ jobs:
|
|||
# run: |
|
||||
# cd /work
|
||||
# make binary-frontend && echo "print contents of /work/dist" && ls -alt /work/dist
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
# - name: Set up QEMU
|
||||
# uses: docker/setup-qemu-action@v2
|
||||
# with:
|
||||
# platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
# Login against a Docker registry except on PR
|
||||
|
@ -59,9 +59,9 @@ jobs:
|
|||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
# platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
file: Dockerfile
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
|
|
@ -2,7 +2,9 @@ FROM node:18.9.0 as frontend-build
|
|||
WORKDIR /usr/src/fastenhealth/frontend
|
||||
#COPY frontend/package.json frontend/yarn.lock ./
|
||||
COPY frontend/package.json ./
|
||||
RUN yarn install --frozen-lockfile
|
||||
#COPY frontend/yarn.lock ./
|
||||
RUN yarn config set registry "http://registry.npmjs.org" \
|
||||
&& yarn install --frozen-lockfile --network-timeout 100000
|
||||
COPY frontend/ ./
|
||||
RUN yarn run build -- --configuration sandbox --output-path=../dist
|
||||
|
||||
|
@ -19,8 +21,9 @@ RUN CGO_ENABLED=0 go build -o /go/bin/fasten ./backend/cmd/fasten/
|
|||
|
||||
# create folder structure
|
||||
RUN mkdir -p /opt/fasten/db \
|
||||
mkdir -p /opt/fasten/web \
|
||||
mkdir -p /opt/fasten/config
|
||||
&& mkdir -p /opt/fasten/web \
|
||||
&& mkdir -p /opt/fasten/config \
|
||||
&& curl -o /opt/fasten/db/fasten.db -L https://github.com/fastenhealth/testdata/raw/main/fasten.db
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ const routes: Routes = [
|
|||
{ path: 'source/:source_id', component: SourceDetailComponent, canActivate: [ CanActivateAuthGuard] },
|
||||
{ path: 'source/:source_id/resource/:resource_id', component: ResourceDetailComponent, canActivate: [ CanActivateAuthGuard] },
|
||||
{ path: 'sources', component: MedicalSourcesComponent, canActivate: [ CanActivateAuthGuard] },
|
||||
{ path: 'sources/callback/:source_type', component: MedicalSourcesComponent, canActivate: [ CanActivateAuthGuard] },
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -22,18 +22,18 @@ export class AppComponent implements OnInit {
|
|||
navbarBackdrop.classList.add('az-navbar-backdrop');
|
||||
document.querySelector('body').appendChild(navbarBackdrop);
|
||||
|
||||
//TODO: onfirst load the header is always shown, why?
|
||||
// seems to be related to the presence of jwt token, and/or auth-interceptor.
|
||||
//determine if we should show the header
|
||||
this.router.events.subscribe(event => this.modifyHeader(event));
|
||||
}
|
||||
|
||||
modifyHeader(event) {
|
||||
if(event instanceof NavigationEnd && event.url?.startsWith('/auth'))
|
||||
{
|
||||
this.showHeader = false;
|
||||
} else {
|
||||
this.showHeader = true;
|
||||
if (event instanceof NavigationEnd) {
|
||||
if (event.url?.startsWith('/auth')) {
|
||||
this.showHeader = false;
|
||||
} else {
|
||||
// console.log("NU")
|
||||
this.showHeader = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,24 +2,24 @@
|
|||
<div class="component-item">
|
||||
<label>Connected</label>
|
||||
<nav class="nav flex-column">
|
||||
<a routerLink="/form/form-elements" routerLinkActive="active" class="nav-link">Active Sources</a>
|
||||
<a routerLink="/form/form-elements" routerLinkActive="active" class="nav-link">Manual</a>
|
||||
<a ngbTooltip="not yet implemented" routerLinkActive="active" class="nav-link">Active Sources</a>
|
||||
<a ngbTooltip="not yet implemented" routerLinkActive="active" class="nav-link">Manual</a>
|
||||
</nav>
|
||||
<label>Categories</label>
|
||||
<nav class="nav flex-column">
|
||||
<a routerLink="/form/form-elements" routerLinkActive="active" class="nav-link">Hospital Networks/Clinics</a>
|
||||
<a routerLink="/form/form-elements" routerLinkActive="active" class="nav-link">Pharmacies</a>
|
||||
<a routerLink="/form/form-elements" routerLinkActive="active" class="nav-link">Insurers</a>
|
||||
<a routerLink="/form/form-elements" routerLinkActive="active" class="nav-link">Laboratoreies</a>
|
||||
<a routerLink="/form/form-elements" routerLinkActive="active" class="nav-link">Wearables</a>
|
||||
<a ngbTooltip="not yet implemented" routerLinkActive="active" class="nav-link">Hospital Networks/Clinics</a>
|
||||
<a ngbTooltip="not yet implemented" routerLinkActive="active" class="nav-link">Pharmacies</a>
|
||||
<a ngbTooltip="not yet implemented" routerLinkActive="active" class="nav-link">Insurers</a>
|
||||
<a ngbTooltip="not yet implemented" routerLinkActive="active" class="nav-link">Laboratories</a>
|
||||
<a ngbTooltip="not yet implemented" routerLinkActive="active" class="nav-link">Wearables</a>
|
||||
</nav>
|
||||
|
||||
<label>Industry</label>
|
||||
<nav class="nav flex-column">
|
||||
<a routerLink="/charts/chartjs" routerLinkActive="active" class="nav-link">Medical</a>
|
||||
<a routerLink="/charts/chartjs" routerLinkActive="active" class="nav-link">Dental</a>
|
||||
<a routerLink="/charts/chartjs" routerLinkActive="active" class="nav-link">Vision</a>
|
||||
<a routerLink="/charts/chartjs" routerLinkActive="active" class="nav-link">Mental</a>
|
||||
<a ngbTooltip="not yet implemented" routerLinkActive="active" class="nav-link">Medical</a>
|
||||
<a ngbTooltip="not yet implemented" routerLinkActive="active" class="nav-link">Dental</a>
|
||||
<a ngbTooltip="not yet implemented" routerLinkActive="active" class="nav-link">Vision</a>
|
||||
<a ngbTooltip="not yet implemented" routerLinkActive="active" class="nav-link">Mental</a>
|
||||
</nav>
|
||||
|
||||
</div><!-- component-item -->
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
</ul>
|
||||
</div><!-- az-header-menu -->
|
||||
<div class="az-header-right">
|
||||
<a routerLink="/" class="az-header-search-link"><i class="fas fa-search"></i></a>
|
||||
<a ngbTooltip="not yet implemented" class="az-header-search-link"><i class="fas fa-search"></i></a>
|
||||
<div class="az-header-message">
|
||||
<a routerLink="/"><i class="typcn typcn-messages"></i></a>
|
||||
</div><!-- az-header-message -->
|
||||
|
@ -33,35 +33,22 @@
|
|||
<p class="az-notification-text">You have 2 unread notification</p>
|
||||
<div class="az-notification-list">
|
||||
<div class="media new">
|
||||
<div class="az-img-user"><img src="assets/images/img2.jpg" alt=""></div>
|
||||
<div class="az-img-user"><img src="assets/sources/aetna.png" alt=""></div>
|
||||
<div class="media-body">
|
||||
<p>Congratulate <strong>Socrates Itumay</strong> for work anniversaries</p>
|
||||
<p><strong>AETNA</strong> added 3 new records</p>
|
||||
<span>Mar 15 12:32pm</span>
|
||||
</div><!-- media-body -->
|
||||
</div><!-- media -->
|
||||
<div class="media new">
|
||||
<div class="az-img-user online"><img src="assets/images/img3.jpg" alt=""></div>
|
||||
<div class="az-img-user online"><img src="assets/sources/cigna.png" alt=""></div>
|
||||
<div class="media-body">
|
||||
<p><strong>Joyce Chua</strong> just created a new blog post</p>
|
||||
<p><strong>CIGNA</strong> added 34 new records</p>
|
||||
<span>Mar 13 04:16am</span>
|
||||
</div><!-- media-body -->
|
||||
</div><!-- media -->
|
||||
<div class="media">
|
||||
<div class="az-img-user"><img src="assets/images/img4.jpg" alt=""></div>
|
||||
<div class="media-body">
|
||||
<p><strong>Althea Cabardo</strong> just created a new blog post</p>
|
||||
<span>Mar 13 02:56am</span>
|
||||
</div><!-- media-body -->
|
||||
</div><!-- media -->
|
||||
<div class="media">
|
||||
<div class="az-img-user"><img src="assets/images/img5.jpg" alt=""></div>
|
||||
<div class="media-body">
|
||||
<p><strong>Adrian Monino</strong> added new comment on your photo</p>
|
||||
<span>Mar 12 10:40pm</span>
|
||||
</div><!-- media-body -->
|
||||
</div><!-- media -->
|
||||
|
||||
</div><!-- az-notification-list -->
|
||||
<div class="dropdown-footer"><a href="">View All Notifications</a></div>
|
||||
<div class="dropdown-footer"><a ngbTooltip="not yet implemented">View All Notifications</a></div>
|
||||
</div><!-- dropdown-menu -->
|
||||
</div><!-- az-header-notification -->
|
||||
<div class="dropdown az-profile-menu" ngbDropdown>
|
||||
|
@ -78,7 +65,7 @@
|
|||
<span>Adminstrator</span>
|
||||
</div><!-- az-header-profile -->
|
||||
|
||||
<a routerLink="/general-pages/profile" class="dropdown-item"><i class="typcn typcn-time"></i> Activity Logs</a>
|
||||
<a ngbTooltip="not yet implemented" class="dropdown-item"><i class="typcn typcn-time"></i> Activity Logs</a>
|
||||
<a (click)="signOut($event)" class="dropdown-item"><i class="typcn typcn-power-outline"></i> Sign Out</a>
|
||||
</div><!-- dropdown-menu -->
|
||||
</div>
|
||||
|
|
|
@ -31,12 +31,14 @@ import {ListDeviceComponent} from './list-generic-resource/list-device.component
|
|||
import {ListDiagnosticReportComponent} from './list-generic-resource/list-diagnostic-report.component';
|
||||
import {ListGoalComponent} from './list-generic-resource/list-goal.component';
|
||||
import { ListFallbackResourceComponent } from './list-fallback-resource/list-fallback-resource.component';
|
||||
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule,
|
||||
BrowserModule,
|
||||
NgxDatatableModule,
|
||||
NgbModule,
|
||||
],
|
||||
declarations: [
|
||||
ComponentsSidebarComponent,
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
export class LighthouseSourceMetadata {
|
||||
authorization_endpoint: string
|
||||
token_endpoint: string
|
||||
introspection_endpoint: string
|
||||
userinfo_endpoint: string
|
||||
|
||||
scopes_supported: string[]
|
||||
issuer: string
|
||||
grant_types_supported: string[]
|
||||
response_types_supported: string[]
|
||||
aud: string
|
||||
code_challenge_methods_supported: string[]
|
||||
|
||||
api_endpoint_base_url: string
|
||||
client_id: string
|
||||
redirect_uri: string
|
||||
|
||||
confidential: boolean
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
export class LighthouseSource {
|
||||
oauth_authorization_endpoint: string
|
||||
oauth_token_endpoint: string
|
||||
oauth_registration_endpoint: string
|
||||
oauth_introspection_endpoint: string
|
||||
oauth_userinfo_endpoint: string
|
||||
oauth_token_endpoint_auth_methods_supported: string
|
||||
|
||||
api_endpoint_base_url: string
|
||||
response_type: string
|
||||
client_id: string
|
||||
scopes: string[]
|
||||
redirect_uri: string
|
||||
aud: string
|
||||
|
||||
confidential: boolean
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import { LighthouseSource } from './lighthouse-source';
|
||||
|
||||
describe('LighthouseSource', () => {
|
||||
it('should create an instance', () => {
|
||||
expect(new LighthouseSource()).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -37,10 +37,13 @@
|
|||
</div><!-- form-group -->
|
||||
<button [disabled]="!userForm.form.valid" type="submit" class="btn btn-az-primary btn-block">Sign In</button>
|
||||
|
||||
<div *ngIf="errorMsg" class="alert alert-danger mt-3" role="alert">
|
||||
<strong>Error</strong> {{errorMsg}}
|
||||
</div>
|
||||
</form>
|
||||
</div><!-- az-signin-header -->
|
||||
<div class="az-signin-footer">
|
||||
<p><a href="">Forgot password?</a></p>
|
||||
<p><a ngbTooltip="not yet implemented">Forgot password?</a></p>
|
||||
<p>Don't have an account? <a routerLink="/auth/signup">Create an Account</a></p>
|
||||
</div><!-- az-signin-footer -->
|
||||
</div><!-- az-card-signin -->
|
||||
|
|
|
@ -11,6 +11,7 @@ import {Router} from '@angular/router';
|
|||
export class AuthSigninComponent implements OnInit {
|
||||
submitted: boolean = false
|
||||
existingUser: User = new User()
|
||||
errorMsg: string = ""
|
||||
|
||||
constructor(private fastenApi: FastenApiService, private router: Router) { }
|
||||
|
||||
|
@ -22,8 +23,9 @@ export class AuthSigninComponent implements OnInit {
|
|||
|
||||
this.fastenApi.signin(this.existingUser.username, this.existingUser.password).subscribe((tokenResp: any) => {
|
||||
console.log(tokenResp);
|
||||
|
||||
this.router.navigateByUrl('/dashboard');
|
||||
}, (err)=>{
|
||||
this.errorMsg = err?.error?.error || "an unknown error occurred during sign-in"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,6 +64,10 @@
|
|||
</div>
|
||||
</div><!-- form-group -->
|
||||
<button [disabled]="!userForm.form.valid" type="submit" class="btn btn-az-primary btn-block">Create Account</button>
|
||||
|
||||
<div *ngIf="errorMsg" class="alert alert-danger mt-3" role="alert">
|
||||
<strong>Error</strong> {{errorMsg}}
|
||||
</div>
|
||||
</form>
|
||||
</div><!-- az-signup-header -->
|
||||
<div class="az-signup-footer">
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import {FastenApiService} from '../../services/fasten-api.service';
|
||||
import {LighthouseSource} from '../../models/lighthouse/lighthouse-source';
|
||||
import {User} from '../../models/fasten/user';
|
||||
import {Router} from '@angular/router';
|
||||
|
||||
|
@ -12,6 +11,7 @@ import {Router} from '@angular/router';
|
|||
export class AuthSignupComponent implements OnInit {
|
||||
submitted: boolean = false
|
||||
newUser: User = new User()
|
||||
errorMsg: string = ""
|
||||
|
||||
constructor(private fastenApi: FastenApiService, private router: Router) { }
|
||||
|
||||
|
@ -25,7 +25,10 @@ export class AuthSignupComponent implements OnInit {
|
|||
console.log(tokenResp);
|
||||
|
||||
this.router.navigateByUrl('/dashboard');
|
||||
})
|
||||
},
|
||||
(err)=>{
|
||||
this.errorMsg = err?.error?.error || "an unknown error occurred during sign-up"
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -29,10 +29,10 @@
|
|||
</nav>
|
||||
|
||||
<nav class="nav">
|
||||
<a class="nav-link" routerLink="/"><i class="far fa-save"></i> Save Report</a>
|
||||
<a class="nav-link" routerLink="/"><i class="far fa-file-pdf"></i> Export to PDF</a>
|
||||
<a class="nav-link" routerLink="/"><i class="far fa-envelope"></i>Send to Email</a>
|
||||
<a class="nav-link" routerLink="/"><i class="fas fa-ellipsis-h"></i></a>
|
||||
<a class="nav-link" routerLink="/" ngbTooltip="not yet implemented"><i class="far fa-save"></i> Save Report</a>
|
||||
<a class="nav-link" routerLink="/" ngbTooltip="not yet implemented"><i class="far fa-file-pdf"></i> Export to PDF</a>
|
||||
<a class="nav-link" routerLink="/" ngbTooltip="not yet implemented"><i class="far fa-envelope"></i>Send to Email</a>
|
||||
<a class="nav-link" routerLink="/" ngbTooltip="not yet implemented"><i class="fas fa-ellipsis-h"></i></a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
|
@ -107,8 +107,8 @@
|
|||
<div class="media">
|
||||
<div ><fa-icon [icon]="['fas', 'calendar']"></fa-icon></div>
|
||||
<div class="media-body"><label >Start Date</label>
|
||||
<div class="date"><span >Sept 01, 2018</span><a
|
||||
href="#"><fa-icon [icon]="['fas', 'caret-down']"></fa-icon></a>
|
||||
<div class="date"><span >Sept 01, 2018</span>
|
||||
<a ngbTooltip="not yet implemented"><fa-icon [icon]="['fas', 'caret-down']"></fa-icon></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -117,8 +117,7 @@
|
|||
<div class="media">
|
||||
<div ><fa-icon [icon]="['fas', 'calendar']"></fa-icon></div>
|
||||
<div class="media-body"><label >End Date</label>
|
||||
<div class="date"><span >Sept 30, 2018</span><a
|
||||
href="#"><fa-icon [icon]="['fas', 'caret-down']"></fa-icon></a>
|
||||
<div class="date"><span >Sept 30, 2018</span><a ngbTooltip="not yet implemented"><fa-icon [icon]="['fas', 'caret-down']"></fa-icon></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -127,8 +126,7 @@
|
|||
<div class="media">
|
||||
<div ><fa-icon [icon]="['fas', 'hospital']"></fa-icon></div>
|
||||
<div class="media-body"><label >Source Type</label>
|
||||
<div class="date"><span >All</span><a
|
||||
href="#"><fa-icon [icon]="['fas', 'caret-down']"></fa-icon></a>
|
||||
<div class="date"><span >All</span><a ngbTooltip="not yet implemented"><fa-icon [icon]="['fas', 'caret-down']"></fa-icon></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import {FastenApiService} from '../../services/fasten-api.service';
|
||||
import {LighthouseSource} from '../../models/lighthouse/lighthouse-source';
|
||||
import {Source} from '../../models/fasten/source';
|
||||
import {Router} from '@angular/router';
|
||||
import {Summary} from '../../models/fasten/summary';
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
import {Component, HostListener, OnInit} from '@angular/core';
|
||||
import {LighthouseService} from '../../services/lighthouse.service';
|
||||
import {FastenApiService} from '../../services/fasten-api.service';
|
||||
import {LighthouseSource} from '../../models/lighthouse/lighthouse-source';
|
||||
import {LighthouseSourceMetadata} from '../../models/lighthouse/lighthouse-source-metadata';
|
||||
import * as Oauth from '@panva/oauth4webapi';
|
||||
import {AuthorizeClaim} from '../../models/lighthouse/authorize-claim';
|
||||
import {Source} from '../../models/fasten/source';
|
||||
import {getAccessTokenExpiration, jwtDecode} from 'fhirclient/lib/lib';
|
||||
import BrowserAdapter from 'fhirclient/lib/adapters/BrowserAdapter';
|
||||
import {Observable, of, throwError} from 'rxjs';
|
||||
import {concatMap, delay, retryWhen} from 'rxjs/operators';
|
||||
import {Observable, of, throwError, fromEvent } from 'rxjs';
|
||||
import {concatMap, delay, retryWhen, timeout, first, map, filter, catchError} from 'rxjs/operators';
|
||||
import * as FHIR from "fhirclient"
|
||||
import {MetadataSource} from '../../models/fasten/metadata-source';
|
||||
import {NgbModal, ModalDismissReasons} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {Location} from '@angular/common';
|
||||
// If you dont import this angular will import the wrong "Location"
|
||||
|
||||
export const retryCount = 24; //wait 2 minutes (5 * 24 = 120)
|
||||
export const retryWaitMilliSeconds = 5000; //wait 5 seconds
|
||||
export const sourceConnectWindowTimeout = 24*5000 //wait 2 minutes (5 * 24 = 120)
|
||||
|
||||
@Component({
|
||||
selector: 'app-medical-sources',
|
||||
|
@ -27,6 +29,9 @@ export class MedicalSourcesComponent implements OnInit {
|
|||
private lighthouseApi: LighthouseService,
|
||||
private fastenApi: FastenApiService,
|
||||
private modalService: NgbModal,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private location: Location,
|
||||
) { }
|
||||
status: { [name: string]: string } = {}
|
||||
|
||||
|
@ -44,6 +49,11 @@ export class MedicalSourcesComponent implements OnInit {
|
|||
this.fastenApi.getMetadataSources().subscribe((metadataSources: {[name:string]: MetadataSource}) => {
|
||||
this.metadataSources = metadataSources
|
||||
|
||||
const callbackSourceType = this.route.snapshot.paramMap.get('source_type')
|
||||
if(callbackSourceType){
|
||||
this.callback(callbackSourceType).then(console.log)
|
||||
}
|
||||
|
||||
this.fastenApi.getSources()
|
||||
.subscribe((sourceList: Source[]) => {
|
||||
|
||||
|
@ -74,123 +84,113 @@ export class MedicalSourcesComponent implements OnInit {
|
|||
this.status[sourceType] = "authorize"
|
||||
|
||||
this.lighthouseApi.getLighthouseSource(sourceType)
|
||||
.subscribe(async (connectData: LighthouseSource) => {
|
||||
console.log(connectData);
|
||||
|
||||
const state = this.uuidV4()
|
||||
|
||||
let authorizationUrl
|
||||
|
||||
//only set if this is not a "confidential" source.
|
||||
let codeVerifier
|
||||
let codeChallenge
|
||||
let codeChallengeMethod
|
||||
|
||||
if(connectData.confidential){
|
||||
authorizationUrl = this.lighthouseApi.generateConfidentialSourceAuthorizeUrl(state, connectData)
|
||||
} else {
|
||||
// https://github.com/panva/oauth4webapi/blob/8eba19eac408bdec5c1fe8abac2710c50bfadcc3/examples/public.ts
|
||||
codeVerifier = Oauth.generateRandomCodeVerifier();
|
||||
codeChallenge = await Oauth.calculatePKCECodeChallenge(codeVerifier);
|
||||
codeChallengeMethod = 'S256';
|
||||
|
||||
authorizationUrl = this.lighthouseApi.generatePKCESourceAuthorizeUrl(codeVerifier, codeChallenge, codeChallengeMethod, state, connectData)
|
||||
}
|
||||
.subscribe(async (sourceMetadata: LighthouseSourceMetadata) => {
|
||||
console.log(sourceMetadata);
|
||||
let authorizationUrl = await this.lighthouseApi.generateSourceAuthorizeUrl(sourceType, sourceMetadata)
|
||||
|
||||
console.log('authorize url:', authorizationUrl.toString());
|
||||
// open new browser window
|
||||
window.open(authorizationUrl.toString(), "_blank");
|
||||
|
||||
//wait for response
|
||||
this.waitForClaimOrTimeout(sourceType, state).subscribe(async (claimData: AuthorizeClaim) => {
|
||||
console.log("claim response:", claimData)
|
||||
this.status[sourceType] = "token"
|
||||
|
||||
let payload: any
|
||||
if(connectData.confidential){
|
||||
|
||||
// we should have an access_token (and optionally a refresh_token) in the claim
|
||||
payload = claimData
|
||||
|
||||
//patient may be returned as patient_id
|
||||
payload.patient = payload.patient ? payload.patient : payload.patient_id
|
||||
|
||||
} else {
|
||||
payload = await this.swapOauthPKCEToken(state, codeVerifier, authorizationUrl, connectData, claimData)
|
||||
}
|
||||
|
||||
|
||||
|
||||
//If payload.patient is not set, make sure we extract the patient ID from the id_token or make an introspection req
|
||||
if(!payload.patient && payload.id_token){
|
||||
//
|
||||
console.log("NO PATIENT ID present, decoding jwt to extract patient")
|
||||
//const introspectionResp = await Oauth.introspectionRequest(as, client, payload.access_token)
|
||||
//console.log(introspectionResp)
|
||||
payload.patient = jwtDecode(payload.id_token, new BrowserAdapter())["profile"].replace(/^(Patient\/)/,'')
|
||||
}
|
||||
|
||||
|
||||
|
||||
//Create FHIR Client
|
||||
|
||||
const sourceCredential: Source = {
|
||||
source_type: sourceType,
|
||||
oauth_authorization_endpoint: connectData.oauth_authorization_endpoint,
|
||||
oauth_token_endpoint: connectData.oauth_token_endpoint,
|
||||
oauth_registration_endpoint: connectData.oauth_registration_endpoint,
|
||||
oauth_introspection_endpoint: connectData.oauth_introspection_endpoint,
|
||||
oauth_userinfo_endpoint: connectData.oauth_userinfo_endpoint,
|
||||
oauth_token_endpoint_auth_methods_supported: connectData.oauth_token_endpoint_auth_methods_supported,
|
||||
api_endpoint_base_url: connectData.api_endpoint_base_url,
|
||||
client_id: connectData.client_id,
|
||||
redirect_uri: connectData.redirect_uri,
|
||||
scopes: connectData.scopes ? connectData.scopes.join(' ') : undefined,
|
||||
patient_id: payload.patient,
|
||||
access_token: payload.access_token,
|
||||
refresh_token: payload.refresh_token,
|
||||
id_token: payload.id_token,
|
||||
code_challenge: codeChallenge,
|
||||
code_verifier: codeVerifier,
|
||||
|
||||
// @ts-ignore - in some cases the getAccessTokenExpiration is a string, which cases failures to store Source in db.
|
||||
expires_at: parseInt(getAccessTokenExpiration(payload, new BrowserAdapter())),
|
||||
confidential: connectData.confidential
|
||||
}
|
||||
|
||||
await this.fastenApi.createSource(sourceCredential).subscribe(
|
||||
(respData) => {
|
||||
delete this.status[sourceType]
|
||||
window.location.reload();
|
||||
|
||||
console.log("source credential create response:", respData)
|
||||
},
|
||||
(err) => {
|
||||
delete this.status[sourceType]
|
||||
window.location.reload();
|
||||
|
||||
console.log(err)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
})
|
||||
// redirect to lighthouse with uri's
|
||||
this.lighthouseApi.redirectWithOriginAndDestination(authorizationUrl.toString(), sourceType)
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@HostListener('window:message', ['$event'])
|
||||
onPostMessage(event: MessageEvent) {
|
||||
console.log("received a message from OAuth popup - "+ event.data, "sleeping 5 seconds")
|
||||
// todo, process event, (retrieve code from passport api and swap for code)
|
||||
setTimeout(() => {
|
||||
console.log("responding to OAuth popup...")
|
||||
event.source.postMessage(JSON.stringify({close:true}),
|
||||
// @ts-ignore
|
||||
event.origin);
|
||||
}, 5000);
|
||||
async callback(sourceType: string) {
|
||||
|
||||
//get the source metadata again
|
||||
await this.lighthouseApi.getLighthouseSource(sourceType)
|
||||
.subscribe(async (sourceMetadata: LighthouseSourceMetadata) => {
|
||||
|
||||
//get required parameters from the URI and local storage
|
||||
const callbackUrlParts = new URL(window.location.href)
|
||||
const fragmentParams = new URLSearchParams(callbackUrlParts.hash.substring(1))
|
||||
const callbackCode = callbackUrlParts.searchParams.get("code") || fragmentParams.get("code")
|
||||
const callbackState = callbackUrlParts.searchParams.get("state") || fragmentParams.get("state")
|
||||
const callbackError = callbackUrlParts.searchParams.get("error") || fragmentParams.get("error")
|
||||
const callbackErrorDescription = callbackUrlParts.searchParams.get("error_description") || fragmentParams.get("error_description")
|
||||
|
||||
//reset the url, removing the params and fragment from the current url.
|
||||
const urlTree = this.router.createUrlTree(["/sources"],{
|
||||
relativeTo: this.route,
|
||||
});
|
||||
this.location.replaceState(urlTree.toString());
|
||||
|
||||
const expectedState = localStorage.getItem(`${sourceType}:state`)
|
||||
localStorage.removeItem(`${sourceType}:state`)
|
||||
|
||||
|
||||
if(callbackError && !callbackCode){
|
||||
//TOOD: print this message in the UI
|
||||
console.error("an error occurred while authenticating to this source. Please try again later", callbackErrorDescription)
|
||||
return
|
||||
}
|
||||
|
||||
console.log("callback code:", callbackCode)
|
||||
this.status[sourceType] = "token"
|
||||
|
||||
let payload: any
|
||||
payload = await this.lighthouseApi.swapOauthToken(sourceType, sourceMetadata,expectedState, callbackState, callbackCode)
|
||||
|
||||
|
||||
//If payload.patient is not set, make sure we extract the patient ID from the id_token or make an introspection req
|
||||
if(!payload.patient && payload.id_token){
|
||||
//
|
||||
console.log("NO PATIENT ID present, decoding jwt to extract patient")
|
||||
//const introspectionResp = await Oauth.introspectionRequest(as, client, payload.access_token)
|
||||
//console.log(introspectionResp)
|
||||
payload.patient = jwtDecode(payload.id_token, new BrowserAdapter())["profile"].replace(/^(Patient\/)/,'')
|
||||
}
|
||||
|
||||
|
||||
|
||||
//Create FHIR Client
|
||||
|
||||
const sourceCredential: Source = {
|
||||
source_type: sourceType,
|
||||
oauth_authorization_endpoint: sourceMetadata.authorization_endpoint,
|
||||
oauth_token_endpoint: sourceMetadata.token_endpoint,
|
||||
oauth_registration_endpoint: "",
|
||||
oauth_introspection_endpoint: sourceMetadata.introspection_endpoint,
|
||||
oauth_userinfo_endpoint: sourceMetadata.userinfo_endpoint,
|
||||
oauth_token_endpoint_auth_methods_supported: "",
|
||||
api_endpoint_base_url: sourceMetadata.api_endpoint_base_url,
|
||||
client_id: sourceMetadata.client_id,
|
||||
redirect_uri: sourceMetadata.redirect_uri,
|
||||
scopes: sourceMetadata.scopes_supported ? sourceMetadata.scopes_supported.join(' ') : undefined,
|
||||
patient_id: payload.patient,
|
||||
access_token: payload.access_token,
|
||||
refresh_token: payload.refresh_token,
|
||||
id_token: payload.id_token,
|
||||
code_challenge: "",
|
||||
code_verifier: "",
|
||||
|
||||
// @ts-ignore - in some cases the getAccessTokenExpiration is a string, which cases failures to store Source in db.
|
||||
expires_at: parseInt(getAccessTokenExpiration(payload, new BrowserAdapter())),
|
||||
confidential: sourceMetadata.confidential
|
||||
}
|
||||
|
||||
await this.fastenApi.createSource(sourceCredential).subscribe(
|
||||
(respData) => {
|
||||
delete this.status[sourceType]
|
||||
// window.location.reload();
|
||||
|
||||
console.log("source credential create response:", respData)
|
||||
//remove item from available sources list, add to connected sources.
|
||||
this.availableSourceList.splice(this.availableSourceList.indexOf(this.metadataSources[sourceType]), 1);
|
||||
this.connectedSourceList.push({source: respData, metadata: this.metadataSources[sourceType]})
|
||||
|
||||
},
|
||||
(err) => {
|
||||
delete this.status[sourceType]
|
||||
// window.location.reload();
|
||||
|
||||
console.log(err)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
uploadSourceBundle(event) {
|
||||
this.uploadedFile = [event.addedFiles[0]]
|
||||
this.fastenApi.createManualSource(event.addedFiles[0]).subscribe(
|
||||
|
@ -226,7 +226,6 @@ export class MedicalSourcesComponent implements OnInit {
|
|||
},
|
||||
(err) => {
|
||||
delete this.status[source.source_type]
|
||||
|
||||
console.log(err)
|
||||
}
|
||||
)
|
||||
|
@ -242,67 +241,4 @@ export class MedicalSourcesComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
private waitForClaimOrTimeout(sourceType: string, state: string): Observable<AuthorizeClaim> {
|
||||
return this.lighthouseApi.getSourceAuthorizeClaim(sourceType, state).pipe(
|
||||
retryWhen(error =>
|
||||
error.pipe(
|
||||
concatMap((error, count) => {
|
||||
if (count <= retryCount && error.status == 500) {
|
||||
return of(error);
|
||||
}
|
||||
return throwError(error);
|
||||
}),
|
||||
delay(retryWaitMilliSeconds)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private 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)
|
||||
);
|
||||
}
|
||||
|
||||
private async swapOauthPKCEToken(state: string, codeVerifier: any, authorizationUrl: URL, connectData: LighthouseSource, claimData: AuthorizeClaim){
|
||||
// @ts-expect-error
|
||||
const client: oauth.Client = {
|
||||
client_id: connectData.client_id,
|
||||
token_endpoint_auth_method: 'none',
|
||||
}
|
||||
|
||||
//check if the oauth_token_endpoint_auth_methods_supported field is set
|
||||
if(connectData.oauth_token_endpoint_auth_methods_supported){
|
||||
let auth_methods = connectData.oauth_token_endpoint_auth_methods_supported.split(",")
|
||||
client.token_endpoint_auth_method = auth_methods[0]
|
||||
}
|
||||
|
||||
const as = {
|
||||
issuer: `${authorizationUrl.protocol}//${authorizationUrl.host}`,
|
||||
authorization_endpoint: connectData.oauth_authorization_endpoint,
|
||||
token_endpoint: connectData.oauth_token_endpoint,
|
||||
introspection_endpoint: connectData.oauth_introspection_endpoint,
|
||||
}
|
||||
|
||||
console.log("STARTING--- Oauth.validateAuthResponse")
|
||||
const params = Oauth.validateAuthResponse(as, client, new URLSearchParams(claimData as any), 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.redirect_uri,
|
||||
codeVerifier,
|
||||
)
|
||||
let payload = await response.json()
|
||||
console.log("ENDING--- Oauth.authorizationCodeGrantRequest", payload)
|
||||
return payload
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,8 +2,6 @@ import { Injectable } from '@angular/core';
|
|||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import {Observable} from 'rxjs';
|
||||
import { Router } from '@angular/router';
|
||||
import {LighthouseSource} from '../models/lighthouse/lighthouse-source';
|
||||
import {environment} from '../../environments/environment';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {ResponseWrapper} from '../models/response-wrapper';
|
||||
import {Source} from '../models/fasten/source';
|
||||
|
@ -42,8 +40,13 @@ export class FastenApiService {
|
|||
signup(newUser: User): Observable<any> {
|
||||
return this._httpClient.post<any>(`${this.getBasePath()}/api/auth/signup`, newUser).pipe(
|
||||
map((res: any) => {
|
||||
localStorage.setItem(this.AUTH_TOKEN_KEY, res.data);
|
||||
return res.data
|
||||
if(res.success){
|
||||
localStorage.setItem(this.AUTH_TOKEN_KEY, res.data);
|
||||
return res.data
|
||||
} else {
|
||||
throw new Error(res.error)
|
||||
}
|
||||
|
||||
}
|
||||
));
|
||||
}
|
||||
|
@ -58,8 +61,12 @@ export class FastenApiService {
|
|||
|
||||
return this._httpClient.post<any>(`${this.getBasePath()}/api/auth/signin`, data).pipe(
|
||||
map((res: any) => {
|
||||
localStorage.setItem(this.AUTH_TOKEN_KEY, res.data);
|
||||
return res.data
|
||||
if(res.success){
|
||||
localStorage.setItem(this.AUTH_TOKEN_KEY, res.data);
|
||||
return res.data
|
||||
} else {
|
||||
throw new Error(res.error)
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@ import {Observable} from 'rxjs';
|
|||
import {environment} from '../../environments/environment';
|
||||
import {map, tap} from 'rxjs/operators';
|
||||
import {ResponseWrapper} from '../models/response-wrapper';
|
||||
import {LighthouseSource} from '../models/lighthouse/lighthouse-source';
|
||||
import {AuthorizeClaim} from '../models/lighthouse/authorize-claim';
|
||||
import {LighthouseSourceMetadata} from '../models/lighthouse/lighthouse-source-metadata';
|
||||
import * as Oauth from '@panva/oauth4webapi';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
@ -15,57 +15,140 @@ export class LighthouseService {
|
|||
constructor(private _httpClient: HttpClient) {
|
||||
}
|
||||
|
||||
getLighthouseSource(sourceType: string): Observable<LighthouseSource> {
|
||||
getLighthouseSource(sourceType: string): Observable<LighthouseSourceMetadata> {
|
||||
return this._httpClient.get<any>(`${environment.lighthouse_api_endpoint_base}/connect/${sourceType}`)
|
||||
.pipe(
|
||||
map((response: ResponseWrapper) => {
|
||||
return response.data as LighthouseSource
|
||||
return response.data as LighthouseSourceMetadata
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
generatePKCESourceAuthorizeUrl(codeVerifier: string, codeChallenge: string, codeChallengeMethod: string, state: string, lighthouseSource: LighthouseSource): URL {
|
||||
|
||||
async generateSourceAuthorizeUrl(sourceType: string, lighthouseSource: LighthouseSourceMetadata): Promise<URL> {
|
||||
const state = this.uuidV4()
|
||||
localStorage.setItem(`${sourceType}:state`, state)
|
||||
|
||||
// generate the authorization url
|
||||
const authorizationUrl = new URL(lighthouseSource.oauth_authorization_endpoint);
|
||||
authorizationUrl.searchParams.set('client_id', lighthouseSource.client_id);
|
||||
authorizationUrl.searchParams.set('code_challenge', codeChallenge);
|
||||
authorizationUrl.searchParams.set('code_challenge_method', codeChallengeMethod);
|
||||
const authorizationUrl = new URL(lighthouseSource.authorization_endpoint);
|
||||
authorizationUrl.searchParams.set('redirect_uri', lighthouseSource.redirect_uri);
|
||||
authorizationUrl.searchParams.set('response_type', 'code');
|
||||
authorizationUrl.searchParams.set('response_type', lighthouseSource.response_types_supported[0]);
|
||||
authorizationUrl.searchParams.set('response_mode', 'fragment');
|
||||
authorizationUrl.searchParams.set('state', state);
|
||||
if(lighthouseSource.scopes && lighthouseSource.scopes.length){
|
||||
authorizationUrl.searchParams.set('scope', lighthouseSource.scopes.join(' '));
|
||||
authorizationUrl.searchParams.set('client_id', lighthouseSource.client_id);
|
||||
if(lighthouseSource.scopes_supported && lighthouseSource.scopes_supported.length){
|
||||
authorizationUrl.searchParams.set('scope', lighthouseSource.scopes_supported.join(' '));
|
||||
}
|
||||
if (lighthouseSource.aud) {
|
||||
authorizationUrl.searchParams.set('aud', lighthouseSource.aud);
|
||||
}
|
||||
|
||||
//this is for providers that support CORS and PKCE (public client auth)
|
||||
if(!lighthouseSource.confidential){
|
||||
// https://github.com/panva/oauth4webapi/blob/8eba19eac408bdec5c1fe8abac2710c50bfadcc3/examples/public.ts
|
||||
const codeVerifier = Oauth.generateRandomCodeVerifier();
|
||||
const codeChallenge = await Oauth.calculatePKCECodeChallenge(codeVerifier);
|
||||
const codeChallengeMethod = lighthouseSource.code_challenge_methods_supported[0]; // 'S256'
|
||||
|
||||
localStorage.setItem(`${sourceType}:code_verifier`, codeVerifier)
|
||||
localStorage.setItem(`${sourceType}:code_challenge`, codeChallenge)
|
||||
localStorage.setItem(`${sourceType}:code_challenge_method`, codeChallengeMethod)
|
||||
|
||||
authorizationUrl.searchParams.set('code_challenge', codeChallenge);
|
||||
authorizationUrl.searchParams.set('code_challenge_method', codeChallengeMethod);
|
||||
}
|
||||
|
||||
return authorizationUrl
|
||||
}
|
||||
|
||||
generateConfidentialSourceAuthorizeUrl(state: string, lighthouseSource: LighthouseSource): URL {
|
||||
// generate the authorization url
|
||||
const authorizationUrl = new URL(lighthouseSource.oauth_authorization_endpoint);
|
||||
authorizationUrl.searchParams.set('client_id', lighthouseSource.client_id);
|
||||
authorizationUrl.searchParams.set('redirect_uri', lighthouseSource.redirect_uri);
|
||||
authorizationUrl.searchParams.set('response_type', 'code');
|
||||
authorizationUrl.searchParams.set('state', state);
|
||||
if(lighthouseSource.scopes && lighthouseSource.scopes.length){
|
||||
authorizationUrl.searchParams.set('scope', lighthouseSource.scopes.join(' '));
|
||||
redirectWithOriginAndDestination(destUrl: string, sourceType: string){
|
||||
const originUrlParts = new URL(window.location.href)
|
||||
originUrlParts.hash = "" //reset hash in-case its present.
|
||||
originUrlParts.pathname = this.pathJoin([originUrlParts.pathname, `callback/${sourceType}`])
|
||||
|
||||
|
||||
const redirectUrlParts = new URL(`${environment.lighthouse_api_endpoint_base}/redirect/${sourceType}`);
|
||||
const redirectParams = new URLSearchParams()
|
||||
redirectParams.set("origin_url", originUrlParts.toString())
|
||||
redirectParams.set("dest_url", destUrl)
|
||||
redirectUrlParts.search = redirectParams.toString()
|
||||
console.log(redirectUrlParts.toString());
|
||||
|
||||
// Simulate a mouse click:
|
||||
window.location.href = redirectUrlParts.toString();
|
||||
}
|
||||
|
||||
async swapOauthToken(sourceType: string, sourceMetadata: LighthouseSourceMetadata, expectedState: string, state: string, code: string){
|
||||
// @ts-expect-error
|
||||
const client: oauth.Client = {
|
||||
client_id: sourceMetadata.client_id
|
||||
}
|
||||
if (lighthouseSource.aud) {
|
||||
authorizationUrl.searchParams.set('aud', lighthouseSource.aud);
|
||||
//this is for providers that support CORS & PKCE (public client auth)
|
||||
let codeVerifier = undefined
|
||||
if(!sourceMetadata.confidential){
|
||||
client.token_endpoint_auth_method = 'none'
|
||||
codeVerifier = localStorage.getItem(`${sourceType}:code_verifier`)
|
||||
|
||||
localStorage.removeItem(`${sourceType}:code_verifier`)
|
||||
localStorage.removeItem(`${sourceType}:code_challenge`)
|
||||
localStorage.removeItem(`${sourceType}:code_challenge_method`)
|
||||
} else {
|
||||
console.log("This is a confidential client, using lighthouse token endpoint.")
|
||||
//if this is a confidential client, we need to "override" token endpoint, and use the Fasten Lighthouse to complete the swap
|
||||
sourceMetadata.token_endpoint = sourceMetadata.redirect_uri.replace("callback", "token")
|
||||
//use a placeholder client_secret (the actual secret is stored in Lighthouse)
|
||||
client.client_secret = "placeholder"
|
||||
client.token_endpoint_auth_method = "client_secret_basic"
|
||||
codeVerifier = "placeholder"
|
||||
}
|
||||
return authorizationUrl
|
||||
|
||||
const as = {
|
||||
issuer: sourceMetadata.issuer,
|
||||
authorization_endpoint: sourceMetadata.authorization_endpoint,
|
||||
token_endpoint: sourceMetadata.token_endpoint,
|
||||
introspection_endpoint: sourceMetadata.introspection_endpoint,
|
||||
}
|
||||
|
||||
console.log("STARTING--- Oauth.validateAuthResponse")
|
||||
const params = Oauth.validateAuthResponse(as, client, new URLSearchParams({"code": code, "state": state}), expectedState)
|
||||
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,
|
||||
sourceMetadata.redirect_uri,
|
||||
codeVerifier,
|
||||
)
|
||||
let payload = await response.json()
|
||||
console.log("ENDING--- Oauth.authorizationCodeGrantRequest", payload)
|
||||
return payload
|
||||
}
|
||||
|
||||
|
||||
getSourceAuthorizeClaim(sourceType: string, state: string): Observable<AuthorizeClaim> {
|
||||
return this._httpClient.get<any>(`${environment.lighthouse_api_endpoint_base}/claim/${sourceType}`, {params: {"state": state}})
|
||||
.pipe(
|
||||
map((response: ResponseWrapper) => {
|
||||
return response.data as AuthorizeClaim
|
||||
})
|
||||
);
|
||||
|
||||
private pathJoin(parts: string[], sep?: string): string{
|
||||
const separator = sep || '/';
|
||||
parts = parts.map((part, index)=>{
|
||||
if (index) {
|
||||
part = part.replace(new RegExp('^' + separator), '');
|
||||
}
|
||||
if (index !== parts.length - 1) {
|
||||
part = part.replace(new RegExp(separator + '$'), '');
|
||||
}
|
||||
return part;
|
||||
})
|
||||
return parts.join(separator);
|
||||
}
|
||||
|
||||
private 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
Loading…
Reference in New Issue