Skip to content
This repository was archived by the owner on Sep 16, 2022. It is now read-only.

Commit 1826746

Browse files
authored
Merge pull request #2 from lca1/dev
MedCo v0.2.1
2 parents a3285b9 + 62d9fb0 commit 1826746

17 files changed

+280
-37
lines changed

deployment/Dockerfile

+4-4
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ COPY deployment/write-gb-config.sh /
3737
RUN chmod +x /write-gb-config.sh
3838

3939
# run-time env
40-
ENV GB_API_URL="http://localhost:8082/IRCT-CL/rest" \
40+
ENV GB_API_URL="http://localhost/pic-sure-api-2/PICSURE" \
4141
GB_URL="http://localhost:4200" \
42-
GB_OIDC_URL="http://localhost:8081/auth/realms/master/protocol/openid-connect" \
43-
GB_OIDC_CLIENT_ID="i2b2" \
42+
GB_OIDC_URL="http://localhost/auth/realms/master/protocol/openid-connect" \
43+
GB_OIDC_CLIENT_ID="medco" \
4444
GB_COTHORITY_KEY_URL="http://localhost/cothority-key.pub.toml" \
45-
GB_GENOMIC_ANNOTATIONS_URL="http://localhost/php" \
45+
GB_GENOMIC_ANNOTATIONS_URL="http://localhost/genomicAnnotations" \
4646
GB_MEDCO_RESULTS_RANDOMIZATION="0" \
4747
MEDCO_NETWORK_NAME="testnetwork" \
4848
MEDCO_NODES_NAME="a,b,c"

src/app/config/env.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"env": "default"
2+
"env": "picsure-medco"
33
}
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1+
import {MedcoQueryType} from "./medco-query-type";
2+
13
export class MedcoNodeResult {
24
// fields from API response
35
nodeName?: string; // todo: make required (implement)
46
networkName?: string; // todo: make required (implement)
7+
queryType: string;
58
encryptedCount: string;
69
encryptionKey: string;
710
timeMeasurements?: object; // todo
8-
patientList?: object; // todo
11+
encryptedPatientList: string[];
912

1013
// fields of extracted data
1114
decryptedCount?: number;
15+
decryptedPatientList?: number[];
16+
parsedQueryType: MedcoQueryType;
1217
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export class MedcoQueryType {
2+
static readonly PATIENT_LIST = new MedcoQueryType("patient_list", "Patient List");
3+
static readonly COUNT_PER_SITE = new MedcoQueryType("count_per_site", "Count per site");
4+
static readonly COUNT_PER_SITE_OBFUSCATED =
5+
new MedcoQueryType("count_per_site_obfuscated", "Count per site (obfuscated)");
6+
static readonly COUNT_PER_SITE_SHUFFLED =
7+
new MedcoQueryType("count_per_site_shuffled", "Count per site (shuffled)");
8+
static readonly COUNT_PER_SITE_SHUFFLED_OBFUSCATED =
9+
new MedcoQueryType("count_per_site_shuffled_obfuscated", "Count per site (shuffled & obfuscated)");
10+
static readonly COUNT_GLOBAL = new MedcoQueryType("count_global", "Global count");
11+
static readonly COUNT_GLOBAL_OBFUSCATED =
12+
new MedcoQueryType("count_global_obfuscated", "Global count (obfuscated)");
13+
14+
static readonly ALL_TYPES = [
15+
MedcoQueryType.PATIENT_LIST,
16+
MedcoQueryType.COUNT_PER_SITE,
17+
MedcoQueryType.COUNT_PER_SITE_OBFUSCATED,
18+
MedcoQueryType.COUNT_PER_SITE_SHUFFLED,
19+
MedcoQueryType.COUNT_PER_SITE_SHUFFLED_OBFUSCATED,
20+
MedcoQueryType.COUNT_GLOBAL,
21+
MedcoQueryType.COUNT_GLOBAL_OBFUSCATED
22+
];
23+
24+
private constructor(public readonly id: string, public readonly name: string) {}
25+
26+
toString() {
27+
return this.id;
28+
}
29+
}

src/app/modules/gb-data-selection-module/gb-data-selection.component.html

+8
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@
3030
Update
3131
</button>
3232
</span>
33+
<span class="float-right">
34+
<label>Query Type: </label>
35+
<select [(ngModel)]="selectedQueryType">
36+
<option *ngFor="let queryType of availableQueryTypes | async" [ngValue]="queryType">
37+
{{queryType.name}}
38+
</option>
39+
</select>
40+
</span>
3341
</div>
3442
</div>
3543
</md2-accordion-header>

src/app/modules/gb-data-selection-module/gb-data-selection.component.ts

+14
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {Component, OnInit} from '@angular/core';
1010
import {FormatHelper} from '../../utilities/format-helper';
1111
import {QueryService} from '../../services/query.service';
1212
import {AppConfig} from '../../config/app.config';
13+
import {MedcoQueryType} from "../../models/picsure-models/i2b2-medco/medco-query-type";
14+
import {Observable} from "rxjs";
1315

1416
@Component({
1517
selector: 'gb-data-selection',
@@ -92,6 +94,18 @@ export class GbDataSelectionComponent implements OnInit {
9294
return this.queryService.isQuerySavingUsed;
9395
}
9496

97+
get availableQueryTypes(): Observable<MedcoQueryType[]> {
98+
return this.queryService.availableQueryTypes;
99+
}
100+
101+
get selectedQueryType(): MedcoQueryType {
102+
return this.queryService.selectedQueryType;
103+
}
104+
105+
set selectedQueryType(queryType: MedcoQueryType) {
106+
this.queryService.selectedQueryType = queryType;
107+
}
108+
95109
update_1(event) {
96110
event.stopPropagation();
97111
this.queryService.update_1();

src/app/modules/gb-medco-results-module/gb-medco-results.component.html

+14
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,18 @@
2121
</tr>
2222
</ng-template>
2323
</p-table>
24+
25+
<hr />
26+
27+
<div *ngIf="patientListAvailable">
28+
<p><b>Patient List</b></p>
29+
<p *ngFor="let nodeResults of medcoResult">
30+
<span>{{nodeResults.nodeName}}:</span>
31+
<i>
32+
<span *ngFor="let patientId of nodeResults.decryptedPatientList">
33+
{{patientId}}
34+
</span>
35+
</i>
36+
</p>
37+
</div>
2438
</div>

src/app/modules/gb-medco-results-module/gb-medco-results.component.ts

+15
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {Component, OnInit} from '@angular/core';
1010
import {MedcoService} from "../../services/picsure-services/medco.service";
1111
import {MedcoNodeResult} from "../../models/picsure-models/i2b2-medco/medco-node-result";
1212
import {Chart} from 'chart.js';
13+
import {MedcoQueryType} from "../../models/picsure-models/i2b2-medco/medco-query-type";
1314

1415
@Component({
1516
selector: 'gb-medco-results',
@@ -21,6 +22,8 @@ export class GbMedcoResultsComponent implements OnInit {
2122

2223
private _resultChart: Chart;
2324
private _medcoResult: MedcoNodeResult[];
25+
private _perSiteCountAvailable: boolean = false;
26+
private _patientListAvailable: boolean = false;
2427

2528
constructor(private medcoService: MedcoService) { }
2629

@@ -61,6 +64,10 @@ export class GbMedcoResultsComponent implements OnInit {
6164
this.medcoService.results.subscribe( (results: MedcoNodeResult[]) => {
6265
this._medcoResult = results;
6366

67+
this._perSiteCountAvailable = this.medcoResult[0].parsedQueryType !== MedcoQueryType.COUNT_GLOBAL &&
68+
this.medcoResult[0].parsedQueryType !== MedcoQueryType.COUNT_GLOBAL_OBFUSCATED;
69+
this._patientListAvailable = this.medcoResult[0].decryptedPatientList.length > 0;
70+
6471
this.resultChart.data.labels = results.map((r) => r.nodeName);
6572
this.resultChart.data.datasets[0].data = results.map((r) => r.decryptedCount);
6673
this.resultChart.update();
@@ -74,4 +81,12 @@ export class GbMedcoResultsComponent implements OnInit {
7481
get medcoResult(): MedcoNodeResult[] {
7582
return this._medcoResult;
7683
}
84+
85+
get perSiteCountAvailable(): boolean {
86+
return this._perSiteCountAvailable;
87+
}
88+
89+
get patientListAvailable(): boolean {
90+
return this._patientListAvailable;
91+
}
7792
}

src/app/services/authentication/authentication-method.ts

+5
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,9 @@ export interface AuthenticationMethod {
5050
*/
5151
logout(): void;
5252

53+
/**
54+
* Returns the authorizations of the user in a list.
55+
*/
56+
authorisations: Observable<string[]>;
57+
5358
}

src/app/services/authentication/authentication.service.ts

+4
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,8 @@ export class AuthenticationService implements OnDestroy {
6161
set accessLevel(value: AccessLevel) {
6262
this._accessLevel = value;
6363
}
64+
65+
get authorisations(): Observable<string[]> {
66+
return this.authenticationMethod.authorisations;
67+
}
6468
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* JWT parsing.
3+
* Credits: Sahil Malik
4+
* Source: https://winsmarts.com/parse-jwt-tokens-using-typescript-9e9c1e186f3e
5+
*/
6+
export class JwtHelper {
7+
private static urlBase64Decode(str: string) {
8+
let output = str.replace(/-/g, '+').replace(/_/g, '/');
9+
switch (output.length % 4) {
10+
case 0:
11+
break;
12+
case 2:
13+
output += '==';
14+
break;
15+
case 3:
16+
output += '=';
17+
break;
18+
default:
19+
// tslint:disable-next-line:no-string-throw
20+
throw 'Illegal base64url string!';
21+
}
22+
return decodeURIComponent((<any>window).escape(window.atob(output)));
23+
}
24+
25+
public static decodeToken(token: string = '') {
26+
if (token === null || token === '') { return { 'upn': '' }; }
27+
const parts = token.split('.');
28+
if (parts.length !== 3) {
29+
30+
throw new Error('JWT must have 3 parts');
31+
}
32+
const decoded = this.urlBase64Decode(parts[1]);
33+
if (!decoded) {
34+
throw new Error('Cannot decode the token');
35+
}
36+
return JSON.parse(decoded);
37+
}
38+
}

src/app/services/authentication/oauth2-authentication.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {Observable} from 'rxjs/Observable';
1414
import {AuthorizationResult} from './authorization-result';
1515
import {Oauth2Token} from './oauth2-token';
1616
import * as moment from 'moment';
17-
import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http';
17+
import {HttpClient, HttpErrorResponse, HttpHeaders, HttpParams} from '@angular/common/http';
1818
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
1919
import {ErrorHelper} from '../../utilities/error-helper';
2020
import {MessageHelper} from '../../utilities/message-helper';
@@ -100,7 +100,7 @@ export class Oauth2Authentication implements AuthenticationMethod {
100100
this.http.post(url, body.toString(), {headers: headers})
101101
.subscribe((result: any) => {
102102
console.log(`Token retrieved.`);
103-
this._token = Oauth2Token.from(result);
103+
this._token = new Oauth2Token(result);
104104
localStorage.setItem('token', JSON.stringify(this._token));
105105
this.authorisation.subscribe((authorisation: AuthorizationResult) => {
106106
resolve(authorisation);
@@ -254,4 +254,14 @@ export class Oauth2Authentication implements AuthenticationMethod {
254254
);
255255
}
256256

257+
get authorisations(): Observable<string[]> {
258+
return this.authorised.map((isAuthorized) => {
259+
if (isAuthorized && this.validToken) {
260+
return this._token.getAuthorisations(this.config.getConfig('oidc-client-id'));
261+
} else {
262+
return [];
263+
}
264+
});
265+
}
266+
257267
}

src/app/services/authentication/oauth2-token.ts

+17-5
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,29 @@
77
*/
88

99
import * as moment from 'moment';
10+
import {JwtHelper} from "./jwt-helper";
1011

1112
export class Oauth2Token {
1213
accessToken: string;
1314
refreshToken: string;
1415
expires: Date;
1516

16-
static from(object: object): Oauth2Token {
17-
return {
18-
accessToken: object['access_token'],
19-
refreshToken: object['refresh_token'],
20-
expires: moment(Date.now()).add(object['expires_in'] as number, 'seconds').toDate()
17+
constructor(object: object) {
18+
this.accessToken = object['access_token'];
19+
this.refreshToken = object['refresh_token'];
20+
this.expires = moment(Date.now()).add(object['expires_in'] as number, 'seconds').toDate();
21+
}
22+
23+
getAuthorisations(oidcClientId: string): string[] {
24+
let parsedToken = JwtHelper.decodeToken(this.accessToken);
25+
26+
if (parsedToken['resource_access'] &&
27+
parsedToken['resource_access'][oidcClientId] &&
28+
parsedToken['resource_access'][oidcClientId]['roles']) {
29+
return parsedToken['resource_access'][oidcClientId]['roles'];
2130
}
31+
32+
return undefined;
2233
}
34+
2335
}

src/app/services/picsure-services/medco.service.ts

+23-8
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {ErrorHelper} from '../../utilities/error-helper';
1515
import {NavbarService} from "../navbar.service";
1616
import {BehaviorSubject} from "rxjs";
1717
import {MedcoNodeResult} from "../../models/picsure-models/i2b2-medco/medco-node-result";
18+
import {MedcoQueryType} from "../../models/picsure-models/i2b2-medco/medco-query-type";
1819

1920
// todo 12/04/19: delayed upgrade of medco-unlynx-js (stay with 0.1.8),
2021
// wait for bug fix of wasm go compiler (?) (changes marked XXX)
@@ -106,22 +107,30 @@ export class MedcoService {
106107
*/
107108
parseMedCoResults(results: MedcoNodeResult[]): number {
108109

109-
// k is 0, 1, 2, ....
110110
for (let k in results) {
111-
// let b64EncodedResultObject = data[k][`medco_results_${k}`];
112-
// let resultObject = JSON.parse(atob(b64EncodedResultObject));
113111

114112
if (this.publicKey !== results[k].encryptionKey) {
115113
console.warn(`Returned public key is different from public key, expect problems (${results[k].encryptionKey})`);
116114
}
117115

118116
results[k].nodeName = `Clinical Site ${k}`; // todo: get from node
119117
results[k].networkName = 'MedCo Network'; // todo: get from node
120-
results[k].decryptedCount = Number(this.decryptInteger(results[k].encryptedCount));
121118
results[k].timeMeasurements = {}; // todo
122119

123-
// results.push(result);
124-
console.log(`${k}: ${results[k].decryptedCount}, times: ${JSON.stringify(results[k].timeMeasurements)}`)
120+
// decrypt count
121+
results[k].decryptedCount = Number(this.decryptInteger(results[k].encryptedCount));
122+
123+
// decrypt patient list, ignore IDs that are zero (= dummies nullified)
124+
results[k].decryptedPatientList = results[k].encryptedPatientList?
125+
results[k].encryptedPatientList
126+
.map((encId) => Number(this.decryptInteger(encId)))
127+
.filter((pId) => pId !== 0):
128+
[];
129+
130+
// parse query type
131+
results[k].parsedQueryType = MedcoQueryType.ALL_TYPES.find((type) => type.id === results[k].queryType);
132+
133+
console.log(`${k}: count=${results[k].decryptedCount}, #patient IDs=${results[k].decryptedPatientList.length}, times: ${JSON.stringify(results[k].timeMeasurements)}`)
125134
}
126135

127136
// randomize results (according to configuration)
@@ -133,9 +142,15 @@ export class MedcoService {
133142
}
134143

135144
this.results.next(results);
136-
this.addMedcoResultsTab();
137145

138-
return results.map((r) => r.decryptedCount).reduce((a, b) => Number(a) + Number(b));
146+
if (results[0].parsedQueryType === MedcoQueryType.COUNT_GLOBAL ||
147+
results[0].parsedQueryType === MedcoQueryType.COUNT_GLOBAL_OBFUSCATED) {
148+
// count is global: all results should be the same, return the first one
149+
return results[0].decryptedCount;
150+
} else {
151+
this.addMedcoResultsTab();
152+
return results.map((r) => r.decryptedCount).reduce((a, b) => Number(a) + Number(b));
153+
}
139154
}
140155

141156
/**

0 commit comments

Comments
 (0)