ng-smart-on-fhir
Version:
[](https://srdc.com.tr/en)
972 lines • 55.5 kB
JavaScript
import * as i0 from '@angular/core';
import { Inject, Injectable, ViewChild, Component, Pipe, NgModule, inject } from '@angular/core';
import * as FHIR from 'fhirclient';
import { Subject, firstValueFrom } from 'rxjs';
import * as i1 from '@angular/router';
import { RouterModule, Router } from '@angular/router';
import * as i4 from '@angular/common';
import { NgStyle, KeyValuePipe, AsyncPipe } from '@angular/common';
import * as i5 from '@angular/forms';
import { FormsModule } from '@angular/forms';
import jsQR from 'jsqr';
import * as jose from 'jose';
import * as pako from 'pako';
import * as i1$1 from '@angular/common/http';
/**
* SMART Authentication Service
* Handles authentication/authorization using the fhirclient library (or offline via SHC/SHL), and stores the state
*/
class SmartAuthService {
constructor(config = {}) {
this.config = config;
// selected login method
this.method = 'offline';
// selected client
this.selectedClient = undefined;
// auth enabled (not public)
this.enabled = true;
this.SAVED_SESSION_KEY = 'SMART_APPS_SESSION';
this.client$ = new Subject();
this.clientPromise = firstValueFrom(this.client$);
this.LAUNCHED_URL = 'SMART_APPS_LAUNCHED_URL';
}
isLoggedIn() {
return !!this.patient || (this.selectedClient?.scope?.includes('launch/patient') ? false : !!this.user);
}
/**
* Login
* @param config - selected client's config
* @param patient - selected patient, if applicable
*/
login(config, patient) {
this.method = 'login';
this.selectedClient = config;
this.enabled = !config.isPublic;
this.patient = patient;
this.saveSession();
if (!config.isPublic) {
FHIR.oauth2.authorize({
iss: config.iss,
redirectUri: config.redirectUri,
clientId: config.clientId,
scope: config.scope,
noRedirect: true
}).then(redirectUrl => {
const [url, params] = redirectUrl.split('?');
let queryParams = params.split('&');
if (config.aud) {
queryParams = queryParams.filter(param => !param.startsWith('aud='));
queryParams.push('aud=' + config.aud);
}
if (config.promptLogin) {
queryParams.push('prompt=login');
}
window.location.href = [url, queryParams.join('&')].join('?');
}, console.error);
}
else if (patient) {
this.client$.next(FHIR.client(config.iss));
}
}
// get public client without authentication
publicClient(iss) {
return FHIR.client(iss);
}
// launch with selected issuer
launch(iss, launch, clientConfig) {
const clientId = this.config.clientId || (this.config.clientIds && this.config.clientIds[iss]);
this.method = 'launch';
this.selectedClient = clientConfig;
this.enabled = true;
this.saveSession({
[this.LAUNCHED_URL]: window.location.href
});
FHIR.oauth2.authorize({
clientId, iss, launch,
redirectUri: this.config.redirectUrl
});
}
// start auth flow
async start() {
const sessionContext = this.restoreSession();
if (sessionContext?.method === 'offline') {
this.offline(sessionContext.patient, sessionContext.shc || []);
}
else if (sessionContext?.selectedClient?.isPublic) {
this.client$.next(this.publicClient((sessionContext?.selectedClient).iss));
}
else {
await FHIR.oauth2.ready().then(async (client) => {
this.checkPatientInToken(client);
this.user = (await client.user.read().catch(() => undefined));
this.patient = await client.patient.read().catch(() => undefined);
this.client$.next(client);
});
}
return sessionContext;
}
// offline mode with SHCs
offline(patient, shc) {
this.patient = patient;
this.user = undefined;
this.method = 'offline';
this.selectedClient = undefined;
this.client$.next(undefined);
this.saveSession({ shc });
}
// store session in the session storage
saveSession(params) {
sessionStorage.setItem(this.SAVED_SESSION_KEY, JSON.stringify({
patient: this.patient,
user: this.user,
method: this.method,
selectedClient: this.selectedClient,
enabled: this.enabled,
...(params || {})
}));
}
// restore session from the local storage
restoreSession() {
const saved = sessionStorage.getItem(this.SAVED_SESSION_KEY);
if (saved) {
const { patient, user, method, selectedClient, enabled, ...rest } = JSON.parse(saved);
this.patient = patient;
this.user = user;
this.method = method;
this.selectedClient = selectedClient;
this.enabled = enabled;
return { patient, user, method, selectedClient, enabled, ...rest };
}
return null;
}
// if the patient is not received properly from the response, but exists in the token, add it to the response
checkPatientInToken(client) {
const sessionId = sessionStorage['SMART_KEY'] && JSON.parse(sessionStorage['SMART_KEY']);
if (sessionId && sessionStorage[sessionId]) {
const session = JSON.parse(sessionStorage[sessionId]);
const token = session.tokenResponse?.access_token;
const id_token = session.tokenResponse?.id_token;
const parsed = JSON.parse(atob(token.split('.')[1]));
let changed = false;
if (!client.user?.id && id_token) {
const parts = id_token.split('.');
const parsedIdToken = JSON.parse(atob(parts[1]));
parsedIdToken.fhirUser = 'Practitioner/' + parsedIdToken.sub;
session.tokenResponse.id_token = [parts[0], btoa(JSON.stringify(parsedIdToken)), parts[2]].join('.');
changed = true;
}
if (!client.getPatientId() && parsed.patient) {
session.tokenResponse.patient = parsed.patient;
changed = true;
}
if (changed) {
sessionStorage[sessionId] = JSON.stringify(session);
window.location.reload();
}
}
}
getClient() {
return this.clientPromise;
}
logout() {
this.clearSession();
this.getClient().then(client => {
const loginClient = this.config?.loginClients?.find(lc => lc.iss === client?.state.serverUrl);
if (loginClient?.logoutUri) {
window.location.href = loginClient.logoutUri;
}
else {
window.location.href = window.location.href.split('#')[0].replace(/shc$/, 'login');
}
}, err => window.location.reload());
}
clearSession() {
sessionStorage.clear();
this.method = 'offline';
this.patient = this.user = undefined;
this.selectedClient = undefined;
this.enabled = true;
}
async getPatient() {
if (this.patient)
return this.patient;
await this.getClient();
return this.patient;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SmartAuthService, deps: [{ token: 'sofConfig' }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SmartAuthService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SmartAuthService, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: ['sofConfig']
}] }] });
/**
* Authenticated FHIR client
*/
class SmartOnFhirService {
get importedResources() {
return this._importedResources;
}
constructor(config = {}, auth) {
this.config = config;
this.auth = auth;
// imported local resources from SHCs
this.importedSHCards = [];
this._importedResources = {};
}
getClient() {
return this.auth.getClient();
}
async ready(callback) {
const client = await this.getClient();
return client ? callback(client) : Promise.reject();
}
getPatient() {
return this.auth.getPatient();
}
// FHIR Search
search(resourceType, ...params) {
return this.ready(client => client.request({
url: this.constructQueryURL(resourceType, params)
}));
}
// FHIR Create (POST/PUT)
create(resource, id) {
return this.ready(client => {
if (id) {
const _resource = { ...resource, id: id };
return client.update(resource);
}
else {
const _resource = { ...resource };
delete _resource.id;
return client.create(_resource);
}
});
}
// FHIR Operation
operation(options) {
const opUrl = (options.resourceType ? options.resourceType + (options.resourceId ? '/' + options.resourceId : '') + '/' : '')
+ '$' + options.operationName;
return this.ready(client => client.request({
method: 'POST',
url: opUrl + (options.queryParams?.length ? this.constructQueryURL('', options.queryParams) : ''),
body: JSON.stringify(options.params || {})
}));
}
// FHIR Request
request(url) {
if (url.length >= 2048) {
return this.ready(client => client.request({
method: 'POST',
url: '',
body: JSON.stringify({
resourceType: 'Bundle',
type: 'batch',
entry: [{
request: {
url,
method: 'GET'
}
}]
})
})).then(bundle => bundle.entry?.at(0)?.resource);
}
return this.ready(client => client.request({
url: url + (url.includes('Observation') ? '&_count=999' : '')
}));
}
logout() {
this.auth.logout();
}
// Construct FHIR API url from the given query
constructQueryURL(resourceType, params) {
return resourceType + '?' + (params?.map(_params => Object.keys(_params).map(key => key + '=' + _params[key]).join('&')).filter(_ => _).join('&') || '');
}
// Import data from SHC
importSHCData(param) {
const { bundle, shc } = param;
this.importedSHCards.push(param);
this._importedResources = this.importedSHCards.map(shc => shc.bundle.entry?.map(entry => entry.resource) || []).flat()
.reduce((o, r) => {
if (!r) {
return;
}
if (!o[r.resourceType]) {
o[r.resourceType] = [];
}
o[r.resourceType].push(r);
return o;
}, {});
const patient = bundle.entry?.find(entry => entry.resource?.resourceType === 'Patient')?.resource;
this.auth.offline(patient, shc);
}
// Return all imported resources from SHC
getAllImportedResources() {
return Object.values(this.importedResources).flat();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SmartOnFhirService, deps: [{ token: 'sofConfig' }, { token: SmartAuthService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SmartOnFhirService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SmartOnFhirService, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: ['sofConfig']
}] }, { type: SmartAuthService }] });
/**
* QR reader & parser component
*/
class QrReaderComponent {
constructor(router) {
this.router = router;
this.scanning = false;
this.qrResult = null;
}
ngOnInit() {
window['jsqr'] = jsQR;
this.startCamera();
}
async startCamera() {
try {
this.videoStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } });
const video = this.videoElement.nativeElement;
video.srcObject = this.videoStream;
video.onloadedmetadata = () => {
video.play();
this.scanning = true;
this.scanQRCode();
};
}
catch (error) {
console.error('Error accessing camera:', error);
}
}
scanQRCode() {
if (!this.scanning)
return;
const video = this.videoElement.nativeElement;
const canvas = this.canvasElement.nativeElement;
const ctx = canvas.getContext('2d');
if (!ctx)
return;
const scan = () => {
if (!this.scanning)
return;
// Ensure video dimensions are available
if (video.videoWidth === 0 || video.videoHeight === 0) {
requestAnimationFrame(scan);
return;
}
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const code = jsQR(imageData.data, imageData.width, imageData.height);
if (code) {
this.checkCode(code);
}
else {
requestAnimationFrame(scan);
}
};
scan();
}
scanFile(event) {
const input = event.target;
if (input.files && input.files[0]) {
const file = input.files[0];
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.src = e.target?.result;
img.onload = () => {
const canvas = this.fileCanvasElement.nativeElement;
const ctx = canvas.getContext('2d');
if (ctx) {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0, img.width, img.height);
const imageData = ctx.getImageData(0, 0, img.width, img.height);
const code = jsQR(imageData.data, imageData.width, imageData.height);
if (code) {
this.checkCode(code);
}
else {
this.error = 'The file does not contain a valid QR.';
}
}
};
};
reader.readAsDataURL(file);
}
}
stopCamera() {
if (this.videoStream) {
this.videoStream.getTracks().forEach(track => track.stop());
}
this.scanning = false;
}
ngOnDestroy() {
this.stopCamera();
}
checkCode(code) {
this.qrResult = code.data;
if (!(this.qrResult.startsWith('shc:/') || this.qrResult.startsWith('shlink:/'))) {
this.error = 'QR doesn\' contain a SMART Health Card or Link';
}
else {
this.router.navigate([!this.qrResult.startsWith('shc:/') ? '/shc' : '/shl'], { fragment: this.qrResult });
this.stopCamera();
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: QrReaderComponent, deps: [{ token: i1.Router }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: QrReaderComponent, selector: "sof-qr-reader", viewQueries: [{ propertyName: "videoElement", first: true, predicate: ["video"], descendants: true, static: true }, { propertyName: "canvasElement", first: true, predicate: ["canvas"], descendants: true, static: true }, { propertyName: "fileCanvasElement", first: true, predicate: ["fileCanvas"], descendants: true, static: true }], ngImport: i0, template: "<div class=\"scanner-container\">\r\n <h2>QR Scanner</h2>\r\n\r\n <!-- Live Camera Scanner -->\r\n <video #video class=\"qr-video\"></video>\r\n <canvas #canvas class=\"qr-canvas\"></canvas>\r\n\r\n <hr>\r\n\r\n <!-- File Upload Scanner -->\r\n <input type=\"file\" accept=\"image/*\" (change)=\"scanFile($event)\">\r\n <canvas #fileCanvas class=\"qr-file-canvas\"></canvas>\r\n\r\n @if (qrResult) {\r\n <p>Scanned QR Code: {{ qrResult }}</p>\r\n }\r\n</div>\r\n", styles: [".scanner-container{text-align:center;width:100%;max-width:500px;margin:auto}.qr-video{width:100%;height:auto;border:2px solid #000}.qr-canvas,.qr-file-canvas{display:none}input[type=file]{margin:10px 0}\n"] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: QrReaderComponent, decorators: [{
type: Component,
args: [{ selector: 'sof-qr-reader', template: "<div class=\"scanner-container\">\r\n <h2>QR Scanner</h2>\r\n\r\n <!-- Live Camera Scanner -->\r\n <video #video class=\"qr-video\"></video>\r\n <canvas #canvas class=\"qr-canvas\"></canvas>\r\n\r\n <hr>\r\n\r\n <!-- File Upload Scanner -->\r\n <input type=\"file\" accept=\"image/*\" (change)=\"scanFile($event)\">\r\n <canvas #fileCanvas class=\"qr-file-canvas\"></canvas>\r\n\r\n @if (qrResult) {\r\n <p>Scanned QR Code: {{ qrResult }}</p>\r\n }\r\n</div>\r\n", styles: [".scanner-container{text-align:center;width:100%;max-width:500px;margin:auto}.qr-video{width:100%;height:auto;border:2px solid #000}.qr-canvas,.qr-file-canvas{display:none}input[type=file]{margin:10px 0}\n"] }]
}], ctorParameters: () => [{ type: i1.Router }], propDecorators: { videoElement: [{
type: ViewChild,
args: ['video', { static: true }]
}], canvasElement: [{
type: ViewChild,
args: ['canvas', { static: true }]
}], fileCanvasElement: [{
type: ViewChild,
args: ['fileCanvas', { static: true }]
}] } });
class LoginComponent {
get patientQuery() { return this._patientQuery; }
set patientQuery(value) {
this._patientQuery = value;
this.searchPatients();
}
hasPreviousPage() { return this.page > 1; }
hasNextPage() { return this.page < (this.total / 10); }
constructor(config, auth, sof, router) {
this.config = config;
this.auth = auth;
this.sof = sof;
this.router = router;
// for Smart Health Card QR scanner
this.scanning = false;
// patient selection for public clients
this.patientSelection = false;
this.patientsLoading = false;
this.patients = [];
this.total = 0;
this.page = 1;
this._patientQuery = '';
}
async login(config) {
if (config.isPublic) {
this.client = this.auth.publicClient(config.iss);
this.searchPatients();
this.patientSelection = true;
this.selectedClient = config;
}
else {
this.auth.login(config);
}
}
/**
* Public client patient selection
* @param patient
*/
selectPatient(patient) {
if (this.selectedClient) {
this.auth.login(this.selectedClient, patient);
this.router.navigate(['/']);
}
}
// public client patient search methods
next() { this.page += 1; this.searchPatients(); }
prev() {
this.page = this.page > 1 ? this.page - 1 : 1;
this.searchPatients();
}
async searchPatients() {
this.patientsLoading = true;
let query = 'Patient?_count=10&_page=' + this.page;
if (this.patientQuery?.trim()) {
query += '&' + this.patientQuery.trim().split(' ').map(name => 'name:contains=' + name).join('&');
}
const bundle = await this.client?.request(query);
if (bundle) {
this.patients = bundle.entry?.map(entry => entry.resource)
.filter(res => !!res) || [];
this.total = bundle.total || 0;
if ((this.page - 1) * 10 > this.total) {
this.page = 1;
}
}
else {
this.patients = [];
this.total = 0;
this.page = 1;
}
this.patientsLoading = false;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LoginComponent, deps: [{ token: 'sofConfig' }, { token: SmartAuthService }, { token: SmartOnFhirService }, { token: i1.Router }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: LoginComponent, selector: "sof-login", ngImport: i0, template: "<div class=\"sof-height-100vh\">\r\n @if (config.logo || config.title) {\r\n <div class=\"logo-n-title\">\r\n @if (config.logo) {\r\n <img [src]=\"config.logo\" class=\"logo\">\r\n }\r\n @if (config.title) {\r\n <div class=\"title sof-text-light\" [style.width]=\"config.title.length / 2 + 'em'\">{{ config.title }}</div>\r\n }\r\n </div>\r\n }\r\n <div class=\"login-container sof-primary-background sof-login-background\">\r\n <div class=\"sof-shadow sof-no-border sof-card sof-width-25 sof-margin-auto\">\r\n <div class=\"sof-card-header sof-login-card-header\"><h4>Login via...</h4></div>\r\n <div class=\"sof-card-body sof-text-center sof-light-background\">\r\n @if (!scanning && !patientSelection) {\r\n <!-- <h4>Login With:</h4>-->\r\n @for (client of config?.loginClients; track $index) {\r\n @if (client.image) {\r\n <a (click)=\"login(client)\" style=\"cursor:pointer;\" class=\"sof-login-client-w-image image\">\r\n <img [src]=\"client.image\">\r\n </a>\r\n } @else {\r\n <button class=\"sof-button sof-text-bold\" [class.sof-button-light]=\"!client.background\" [class.sof-text-dark]=\"!client.color\"\r\n [ngStyle]=\"{background: client.background}\" (click)=\"login(client)\">\r\n <div class=\"text-content\" [ngStyle]=\"{color: client.color}\">{{client.label}}</div>\r\n </button>\r\n }\r\n }\r\n @for (client of config?.launchClients; track $index) {\r\n @if (client.image) {\r\n <a [href]=\"client.url\" class=\"image sof-button sof-button-light\" style=\"cursor:pointer;\">\r\n <img [src]=\"client.image\">\r\n </a>\r\n }\r\n }\r\n @for (client of config?.launchClients; track $index) {\r\n @if (!client.image) {\r\n <a class=\"sof-button sof-button-secondary sof-button-login sof-text-bold\" [href]=\"client.url\">{{client.label}}</a>\r\n }\r\n }\r\n @if (config?.shcLoginEnabled) {\r\n <!-- <span class=\"mt-5\">or</span>-->\r\n <a class=\"sof-button sof-button-secondary\" (click)=\"scanning = true\">\r\n <i class=\"sof-icon sof-icon-qr-code\"></i> SMART Health Card\r\n </a>\r\n }\r\n } @else if (scanning) {\r\n <sof-qr-reader #reader></sof-qr-reader>\r\n @if (reader.error) {\r\n <div class=\"sof-alert sof-alert-danger\">\r\n {{ reader.error }}\r\n </div>\r\n }\r\n <button class=\"sof-button sof-button-danger\" (click)=\"scanning = false\">Cancel</button>\r\n } @else if (patientSelection) {\r\n <div class=\"sof-width-100\" style=\"max-height: 50vh; overflow-y: auto\">\r\n <table class=\"sof-table\">\r\n <tbody>\r\n <tr>\r\n <td>\r\n <button class=\"sof-button sof-button-danger sof-mr-1\" (click)=\"patientSelection = false\">Cancel</button>\r\n <button class=\"sof-button sof-button-primary sof-mr-1\" [class.disabled]=\"!hasPreviousPage()\" (click)=\"prev()\"><i class=\"sof-icon sof-icon-caret-left\"></i></button>\r\n <input class=\"sof-form-control\" style=\"display: inline-block; width: auto\" placeholder=\"Search\" [(ngModel)]=\"patientQuery\">\r\n <button class=\"sof-button sof-button-primary sof-ml-1\" [class.disabled]=\"!hasNextPage()\" (click)=\"next()\"><i class=\"sof-icon sof-icon-caret-right\"></i></button>\r\n </td>\r\n </tr>\r\n @if(patientsLoading) {\r\n <tr><td>\r\n <div class=\"sof-spinner\" role=\"status\">\r\n <span class=\"sof-hidden\">Loading...</span>\r\n </div>\r\n </td></tr>\r\n } @else {\r\n @for (patient of patients; track patient.id) {\r\n <tr style=\"cursor: pointer\" (click)=\"selectPatient(patient)\"><td>{{patient.name?.at(0)?.given}} {{patient.name?.at(0)?.family}}</td></tr>\r\n }\r\n <tr><td class=\"sof-text-bold sof-text-gray\" style=\"font-size: .75em\">{{(page - 1) * patients.length}} - {{page * patients.length}}/{{total}}</td></tr>\r\n }\r\n </tbody>\r\n </table>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\n", styles: [".login-container{margin:-1rem!important;width:calc(100% + 2rem)!important;height:calc(100% + 2rem)!important}.login-container>.sof-card{min-width:500px;position:relative;top:50%;transform:translateY(-50%);min-height:500px}.login-container>.sof-card .sof-card-body{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-around;gap:1em;align-items:stretch}.login-container>.sof-card .sof-card-body>a.image{display:flex;justify-content:space-around;align-items:center}.login-container>.sof-card .sof-card-body>a.image img{width:150px}.login-container>.sof-card .sof-card-body>button .text-content{width:150px;white-space:wrap}.login-container>.sof-card .sof-card-body>a:not(.image){width:100%;display:flex;text-align:center;align-items:center;justify-content:center;gap:.5em}.logo-n-title{position:fixed;left:2em;top:2em;width:calc(100vw - 4em);display:flex;gap:1em}.logo-n-title .logo{width:150px}.logo-n-title .title{max-width:calc(85vw - 150px);font-size:2em;font-weight:700;border-left:.1em solid;padding-left:.4em;display:flex;align-items:center;white-space:break-spaces}\n"], dependencies: [{ kind: "directive", type: i4.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i5.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i5.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i5.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: QrReaderComponent, selector: "sof-qr-reader" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LoginComponent, decorators: [{
type: Component,
args: [{ selector: 'sof-login', template: "<div class=\"sof-height-100vh\">\r\n @if (config.logo || config.title) {\r\n <div class=\"logo-n-title\">\r\n @if (config.logo) {\r\n <img [src]=\"config.logo\" class=\"logo\">\r\n }\r\n @if (config.title) {\r\n <div class=\"title sof-text-light\" [style.width]=\"config.title.length / 2 + 'em'\">{{ config.title }}</div>\r\n }\r\n </div>\r\n }\r\n <div class=\"login-container sof-primary-background sof-login-background\">\r\n <div class=\"sof-shadow sof-no-border sof-card sof-width-25 sof-margin-auto\">\r\n <div class=\"sof-card-header sof-login-card-header\"><h4>Login via...</h4></div>\r\n <div class=\"sof-card-body sof-text-center sof-light-background\">\r\n @if (!scanning && !patientSelection) {\r\n <!-- <h4>Login With:</h4>-->\r\n @for (client of config?.loginClients; track $index) {\r\n @if (client.image) {\r\n <a (click)=\"login(client)\" style=\"cursor:pointer;\" class=\"sof-login-client-w-image image\">\r\n <img [src]=\"client.image\">\r\n </a>\r\n } @else {\r\n <button class=\"sof-button sof-text-bold\" [class.sof-button-light]=\"!client.background\" [class.sof-text-dark]=\"!client.color\"\r\n [ngStyle]=\"{background: client.background}\" (click)=\"login(client)\">\r\n <div class=\"text-content\" [ngStyle]=\"{color: client.color}\">{{client.label}}</div>\r\n </button>\r\n }\r\n }\r\n @for (client of config?.launchClients; track $index) {\r\n @if (client.image) {\r\n <a [href]=\"client.url\" class=\"image sof-button sof-button-light\" style=\"cursor:pointer;\">\r\n <img [src]=\"client.image\">\r\n </a>\r\n }\r\n }\r\n @for (client of config?.launchClients; track $index) {\r\n @if (!client.image) {\r\n <a class=\"sof-button sof-button-secondary sof-button-login sof-text-bold\" [href]=\"client.url\">{{client.label}}</a>\r\n }\r\n }\r\n @if (config?.shcLoginEnabled) {\r\n <!-- <span class=\"mt-5\">or</span>-->\r\n <a class=\"sof-button sof-button-secondary\" (click)=\"scanning = true\">\r\n <i class=\"sof-icon sof-icon-qr-code\"></i> SMART Health Card\r\n </a>\r\n }\r\n } @else if (scanning) {\r\n <sof-qr-reader #reader></sof-qr-reader>\r\n @if (reader.error) {\r\n <div class=\"sof-alert sof-alert-danger\">\r\n {{ reader.error }}\r\n </div>\r\n }\r\n <button class=\"sof-button sof-button-danger\" (click)=\"scanning = false\">Cancel</button>\r\n } @else if (patientSelection) {\r\n <div class=\"sof-width-100\" style=\"max-height: 50vh; overflow-y: auto\">\r\n <table class=\"sof-table\">\r\n <tbody>\r\n <tr>\r\n <td>\r\n <button class=\"sof-button sof-button-danger sof-mr-1\" (click)=\"patientSelection = false\">Cancel</button>\r\n <button class=\"sof-button sof-button-primary sof-mr-1\" [class.disabled]=\"!hasPreviousPage()\" (click)=\"prev()\"><i class=\"sof-icon sof-icon-caret-left\"></i></button>\r\n <input class=\"sof-form-control\" style=\"display: inline-block; width: auto\" placeholder=\"Search\" [(ngModel)]=\"patientQuery\">\r\n <button class=\"sof-button sof-button-primary sof-ml-1\" [class.disabled]=\"!hasNextPage()\" (click)=\"next()\"><i class=\"sof-icon sof-icon-caret-right\"></i></button>\r\n </td>\r\n </tr>\r\n @if(patientsLoading) {\r\n <tr><td>\r\n <div class=\"sof-spinner\" role=\"status\">\r\n <span class=\"sof-hidden\">Loading...</span>\r\n </div>\r\n </td></tr>\r\n } @else {\r\n @for (patient of patients; track patient.id) {\r\n <tr style=\"cursor: pointer\" (click)=\"selectPatient(patient)\"><td>{{patient.name?.at(0)?.given}} {{patient.name?.at(0)?.family}}</td></tr>\r\n }\r\n <tr><td class=\"sof-text-bold sof-text-gray\" style=\"font-size: .75em\">{{(page - 1) * patients.length}} - {{page * patients.length}}/{{total}}</td></tr>\r\n }\r\n </tbody>\r\n </table>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\n", styles: [".login-container{margin:-1rem!important;width:calc(100% + 2rem)!important;height:calc(100% + 2rem)!important}.login-container>.sof-card{min-width:500px;position:relative;top:50%;transform:translateY(-50%);min-height:500px}.login-container>.sof-card .sof-card-body{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-around;gap:1em;align-items:stretch}.login-container>.sof-card .sof-card-body>a.image{display:flex;justify-content:space-around;align-items:center}.login-container>.sof-card .sof-card-body>a.image img{width:150px}.login-container>.sof-card .sof-card-body>button .text-content{width:150px;white-space:wrap}.login-container>.sof-card .sof-card-body>a:not(.image){width:100%;display:flex;text-align:center;align-items:center;justify-content:center;gap:.5em}.logo-n-title{position:fixed;left:2em;top:2em;width:calc(100vw - 4em);display:flex;gap:1em}.logo-n-title .logo{width:150px}.logo-n-title .title{max-width:calc(85vw - 150px);font-size:2em;font-weight:700;border-left:.1em solid;padding-left:.4em;display:flex;align-items:center;white-space:break-spaces}\n"] }]
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: ['sofConfig']
}] }, { type: SmartAuthService }, { type: SmartOnFhirService }, { type: i1.Router }] });
class LaunchComponent {
constructor(config, route, auth) {
this.config = config;
this.route = route;
this.auth = auth;
// Handle the query parameters to be used in the Smart App Launch flow
this.route.queryParams.subscribe(params => {
const iss = decodeURIComponent(params['iss']);
const launch = params['launch'];
// start launch flow
this.auth.launch(iss, launch);
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LaunchComponent, deps: [{ token: 'sofConfig' }, { token: i1.ActivatedRoute }, { token: SmartAuthService }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: LaunchComponent, selector: "sof-launch", ngImport: i0, template: "<div class=\"w-100 h-100 position-fixed bg-light\" style=\"line-height: 100vh; text-align: center\">\r\n <span class=\"spinner-border spinner-border-sm\" aria-hidden=\"true\"></span> Processing Authorization...\r\n</div>\r\n", styles: [""] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LaunchComponent, decorators: [{
type: Component,
args: [{ selector: 'sof-launch', template: "<div class=\"w-100 h-100 position-fixed bg-light\" style=\"line-height: 100vh; text-align: center\">\r\n <span class=\"spinner-border spinner-border-sm\" aria-hidden=\"true\"></span> Processing Authorization...\r\n</div>\r\n" }]
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: ['sofConfig']
}] }, { type: i1.ActivatedRoute }, { type: SmartAuthService }] });
class CallbackComponent {
constructor(router, route, auth) {
this.router = router;
this.route = route;
this.auth = auth;
}
ngOnInit() {
// Subscribe to route data to decide redirection URL after successful authentication
this.route.data.subscribe(data => {
// Call authentication service to handle code/token exchange
this.auth.start().then(() => {
// redirect to specified page after the token is successfully retrieved
this.router.navigate([data['redirectTo']]);
}, (error) => {
// set error message if failed
this.error = error?.message || error?.toString() || 'Unknown error occurred.';
});
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CallbackComponent, deps: [{ token: i1.Router }, { token: i1.ActivatedRoute }, { token: SmartAuthService }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: CallbackComponent, selector: "sof-callback", ngImport: i0, template: "<div class=\"sof-width-100 sof-height-100 sof-light-background\" style=\"position: fixed; line-height: 100vh; text-align: center\">\r\n @if (!error) {\r\n <span class=\"sof-spinner\" style=\"vertical-align: middle\" aria-hidden=\"true\"></span> Processing Authorization...\r\n } @else {\r\n <div style=\"display: inline-block; line-height: 2em\">\r\n <i class=\"sof-text-danger sof-icon sof-icon-exclamation\" style=\"font-size: 2em\"></i>\r\n <p>{{error}}asdsadasdasd</p>\r\n </div>\r\n }\r\n</div>\r\n", styles: [""] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CallbackComponent, decorators: [{
type: Component,
args: [{ selector: 'sof-callback', template: "<div class=\"sof-width-100 sof-height-100 sof-light-background\" style=\"position: fixed; line-height: 100vh; text-align: center\">\r\n @if (!error) {\r\n <span class=\"sof-spinner\" style=\"vertical-align: middle\" aria-hidden=\"true\"></span> Processing Authorization...\r\n } @else {\r\n <div style=\"display: inline-block; line-height: 2em\">\r\n <i class=\"sof-text-danger sof-icon sof-icon-exclamation\" style=\"font-size: 2em\"></i>\r\n <p>{{error}}asdsadasdasd</p>\r\n </div>\r\n }\r\n</div>\r\n" }]
}], ctorParameters: () => [{ type: i1.Router }, { type: i1.ActivatedRoute }, { type: SmartAuthService }] });
class CamelCaseSpacedPipe {
transform(value) {
return value.replaceAll(/([A-Z])([A-Z])([a-z])|([a-z])([A-Z])/g, '$1$4 $2$3$5');
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CamelCaseSpacedPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "17.3.12", ngImport: i0, type: CamelCaseSpacedPipe, name: "camelCaseSpaced" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CamelCaseSpacedPipe, decorators: [{
type: Pipe,
args: [{
name: 'camelCaseSpaced'
}]
}] });
/**
* SMART Health Link Service
* Executes the requests, parses and validates SMART Health Links as defined in the HL7 documentations
*/
class ShlService {
get emptyBundle() {
return {
resourceType: 'Bundle',
type: 'document',
entry: []
};
}
constructor(http) {
this.http = http;
}
async handleShl(shl) {
if (shl.startsWith('shlink:/')) {
shl = shl.slice('shlink:/'.length);
}
const metadata = JSON.parse(atob(shl));
if (!metadata.url) {
throw new Error('URL is missing in SMART Health Link');
}
let payload;
if (metadata.flag?.includes('U')) {
payload = await firstValueFrom(this.http.get(metadata.url + '?recipient=' + window.location.origin))
.then((data) => data.files.filter((file) => file.contentType === 'application/smart-health-card' && file.embedded).map((file) => file.embedded));
}
else {
let passcode;
if (metadata.flag?.includes('P')) {
passcode = prompt('Enter the passcode for SMART Health Link');
}
payload = await firstValueFrom(this.http.post(metadata.url, {
recipient: window.location.origin,
passcode
})).then((data) => data.files.filter((file) => file.contentType === 'application/smart-health-card' && file.embedded).map((file) => file.embedded));
}
if (!payload) {
throw new Error('Cannot get SMART Health Link content.');
}
const shcs = await this.decryptSHLPayloadAndGetSHCs(payload, metadata.key);
const bundles = await Promise.all(shcs.map(shc => this.getSingleBundle(shc)));
return {
label: metadata.label,
shc: shcs,
verified: await Promise.all(shcs.map(_shc => this.verifyShc(_shc))).then(results => results.every(value => value)),
bundle: Object.assign(this.emptyBundle, {
entry: bundles.filter(_ => _).map(bundle => bundle.entry).reduce((arr, entries) => arr.concat(entries), [])
})
};
}
async decryptSHLPayloadAndGetSHCs(payload, key) {
const SHCs = [];
for (const encrypted of payload) {
const data = await jose.compactDecrypt(encrypted, jose.base64url.decode(key));
SHCs.push(...JSON.parse(new TextDecoder().decode(data.plaintext)).verifiableCredential);
}
return SHCs;
}
async handleShc(shc) {
if (shc.startsWith('shc:/')) {
shc = shc.slice('shc:/'.length);
}
return {
shc: [shc],
bundle: await this.getSingleBundle(shc),
verified: await this.verifyShc(shc)
};
}
async getSingleBundle(shc) {
return await this.parseShc(shc).then(data => data.vc.credentialSubject.fhirBundle);
}
async parseShc(shc) {
try {
const [h, p, s] = shc.split('.').map((part) => this.base64UrlToBytes(part));
return JSON.parse(this.inflate(p));
}
catch (error) {
console.error('Error parsing SMART Health Card:', error);
throw error;
}
}
async verifyShc(shc) {
try {
await jose.compactVerify(shc, jose.createLocalJWKSet(await this.fetchJWKs(shc)));
return true;
}
catch (err) {
return false;
}
}
async fetchJWKs(shc) {
try {
const card = JSON.parse(this.inflate(this.base64UrlToBytes(shc.split('.')[1])));
const issuer = card.iss;
const jwksUrl = `${issuer.replace(/\/$/, '')}/.well-known/jwks`;
const jwksResponse = await firstValueFrom(this.http.get(jwksUrl));
return jwksResponse;
}
catch (error) {
console.error('Error fetching JWKs:', error);
throw new Error('Unable to fetch JWKs for signature verification');
}
}
inflate(data) {
try {
const decompressed = pako.inflateRaw(data, { to: 'string' });
return decompressed;
}
catch (error) {
console.error('Error inflating data:', error);
throw new Error('Failed to inflate data');
}
}
base64UrlToBytes(base64Url) {
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const binaryString = atob(base64);
return new Uint8Array([...binaryString].map((char) => char.charCodeAt(0)));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ShlService, deps: [{ token: i1$1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ShlService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ShlService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [{ type: i1$1.HttpClient }] });
/**
* Component for searching and validating Smart Health Cards or Smart Health Links
*/
class ShcShlHandlerComponent {
get patientName() {
return this.patient?.name?.map(name => (name.given?.join(' ') || '') + ' ' + name.family).join(', ');
}
get patientAge() {
if (this.patient?.birthDate) {
return Math.ceil((Date.now() - new Date(this.patient.birthDate).getTime()) / (1000 * 3600 * 24 * 365));
}
else {
return '';
}
}
constructor(route, auth, shl, sof) {
this.route = route;
this.auth = auth;
this.shl = shl;
this.sof = sof;
this._window = window;
this.invalidSignature = false;
this.auth.getPatient().then(patient => this.patient = patient);
route.fragment.subscribe(async (fragment) => {
try {
if (fragment?.startsWith('shlink:/')) {
this.sof.importSHCData(await this.shl.handleShl(fragment));
}
else if (fragment?.startsWith('shc:/')) {
this.sof.importSHCData(await this.shl.handleShc(fragment));
}
else {
console.log(fragment);
}
}
catch (error) {
if (error instanceof jose.errors.JWSSignatureVerificationFailed) {
this.invalidSignature = true;
}
}
});
}
ngOnInit() {
}
logout() {
this.sof.logout();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ShcShlHandlerComponent, deps: [{ token: i1.ActivatedRoute }, { token: SmartAuthService }, { token: ShlService }, { token: SmartOnFhirService }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: ShcShlHandlerComponent, selector: "sof-shc-shl-handler", ngImport: i0, template: `
<div class="container">
@if (invalidSignature) {
<div class="sof-alert sof-alert-danger">
Issuer verification failed for the Smart Health Card!
</div>
<button class="sof-button sof-button-danger" (click)="logout()"><i class="sof-icon sof-icon-caret-left"></i> Back</button>
<button class="sof-button sof-button-warning sof-ml-1 sof-mt-1" (click)="invalidSignature = false"><i class="sof-icon sof-icon-exclamation"></i> Ignore and Continue</button>
} @else {
<table class="sof-table" id="shc-content">
<tbody>
<tr>
<td><b>Patient</b></td>
<td>{{ patientName }}</td>
</tr>
<tr>
<td><b>Age</b></td>
<td>{{ patientAge }}</td>
</tr>
<tr>
<td><b>Gender</b></td>
<td>{{ patient?.gender }}</td>
</tr>
@for (resources of sof.importedResources | keyvalue; track resources.key) {
<tr>
<td><b>{{resources.key | camelCaseSpaced}}</b></td>
<td><span class="sof-badge sof-primary-background sof-text-light">{{resources.value?.length}}</span></td>
</tr>
}
<tr>
<td colspan="2" class="text-end">
<button class="sof-button sof-button-primary" routerLink="/">Continue <i class="sof-icon sof-icon-caret-right"></i></button>
</td>
</tr>
</tbody>
</table>
}
</div>
<style>
#shc-content td:first-child {
width: 25em;
max-width: 100%;
}
</style>
`, isInline: true, styles: ["\n #shc-content td:first-child {\n width: 25em;\n max-width: 100%;\n }\n "], dependencies: [{ kind: "directive", type: i1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "pipe", type: i4.KeyValuePipe, name: "keyvalue" }, { kind: "pipe", type: CamelCaseSpacedPipe, name: "camelCaseSpaced" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ShcShlHandlerComponent, decorators: [{
type: Component,
args: [{
selector: 'sof-shc-shl-handler',
template: `
<div class="container">
@if (invalidSignature) {
<div class="sof-alert sof-alert-danger">
Issuer verification failed for the Smart Health Card!
</div>
<button class="sof-button sof-button-danger" (click)="logout()"><i class="sof-icon sof-icon-caret-left"></i> Back</button>
<button class="sof-button sof-button-warning sof-ml-1 sof-mt-1" (click)="invalidSignature = false"><i class="sof-icon sof-icon-exclamation"></i> Ignore and Continue</button>
} @else {
<table class="sof-table" id="shc-content">
<tbody>
<tr>
<td><b>Patient</b></td>
<td>{{ patientName }}</td>
</tr>
<tr>
<td><b>Age</b></td>
<td>{{ patientAge }}</td>
</tr>
<tr>
<td><b>Gender</b></td>
<td>{{ patient?.gender }}</td>
</tr>
@for (resources of sof.importedResources | keyvalue; track resources.key) {
<tr>
<td><b>{{resources.key | camelCaseSpaced}}</b></td>
<td><span class="sof-badge sof-primary-background sof-text-light">{{resources.value?.length}}</span></td>
</tr>
}
<tr>
<td colspan="2" class="text-end">
<button class="sof-button sof-button-primary" routerLink="/">Continue <i class="sof-icon sof-icon-caret-right"></i></button>
</td>
</tr>
</tbody>
</table>
}
</div>
<style>
#shc-content td:first-child {
width: 25em;
max-width: 100%;
}
</style>
`
}]
}], ctorParameters: () => [{ type: i1.ActivatedRoute }, { type: SmartAuthService }, { type: ShlService }, { type: SmartOnFhirService }] });
class SmartOnFhirModule {
static forRoot(config) {
return {
ngModule: SmartOnFhirModule,
providers: [SmartOnFhirService, { provide: 'sofConfig', useValue: config }, SmartAuthService]
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SmartOnFhirModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "17.3.12", ngImport: i0, type: SmartOnFhirModule, declarations: [LoginComponent, LaunchComponent, CallbackComponent, QrReaderComponent, CamelCaseSpacedPipe, ShcShlHandlerComponent], imports: [NgStyle, KeyValuePipe, RouterModule, AsyncPipe, FormsModule] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SmartOnFhirModule, providers: [SmartOnFhirService, SmartAuthService], imports: [RouterModule, FormsModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SmartOnFhirModule, decorators: [{
type: NgModule,
args: [{
imports: [
NgStyle, KeyValuePipe, RouterModule, AsyncPipe, FormsModule
],
exports: [],
declarations: [LoginComponent, LaunchComponent, CallbackComponent, QrReaderComponent, CamelCaseSpacedPipe, ShcShlHandlerComponent],
providers: [SmartOnFhirService, SmartAuthService],
}]
}] });
const redirectUnauthorizedToLogin = async function (route, state) {
const router = inject(Router);
const auth = inject(SmartAuthService);
const sof = inject(SmartOnFhirService);
const shl = inject(ShlService);
if (auth.isLoggedIn()) {
return true;
}
try {
const context = auth.restoreSession();
if (context?.shc) {
await Promise.all(context.shc.map(async (shc) => sof.importSHCData(await shl.handleShc(shc))));
}
await auth.start();
return auth.isLoggedIn();
}
catch (err) { }
if (auth.isLoggedIn()) {
return true;
}
await router.navigate(['/login']);
return false;
};
/**
* Routes wrapper to add required components to execute Smart App Launch and Login flows
* @param routes - Other application routes
* @param redirectTo - Redirection after login
* @param method - Allowed authentication flows: launch/client(login)/both
* @param redirectToLoginIfUnauthorized - If true, adds auth guards to the application components
* @param enableShc - If true, offline mode is allowed using Smart Health Cards
*/
const withSmartHandlerRoutes = (routes, redirectTo, method, redirectToLoginIfUnauthorized, enableShc) => {
const smartRoutes = [{
path: 'callback',
component: CallbackComponent,
data: { redirectTo }
}];
if (method !== 'client') {
smartRoutes.push({
path: 'launch',
component: LaunchComponent
});
}
if (method !== 'launch') {
smartRoutes.push({
path: 'login',
component: LoginComponent
});
}
if (enableShc) {
smartRoutes.push({
path: 'shl',
component: ShcShlHandlerComponent
}, {
path: 'shc',
component: ShcShlHandlerComponent
});
}
return [
...routes.map(route => {
if (redirectToLoginIfUnauthorized) {
route.canActivate = (route.canActivate || []).concat([redirectUnauthorizedToLogin]);
}
return route;
}),
...smartRoutes
];
};
/*
* Public API Surface of ng-smart-on-fhir
*/
/**
* Generated bundle index. Do not edit.
*/
export { SmartAuthService, SmartOnFhirModule, SmartOnFhirService, withSmartHandlerRoutes };
//# sourceMappingURL=ng-smart-on-fhir.mjs.map