From 5966e702580479e25e04816ddda8189d5337114c Mon Sep 17 00:00:00 2001 From: tamslo Date: Fri, 8 Nov 2024 18:13:05 +0100 Subject: [PATCH] feat(#660): refactor lab class --- app/generate_screendocs/sequence_utils.dart | 2 +- app/integration_test/login_test.dart | 1 - app/lib/common/constants.dart | 4 - .../common/models/userdata/lab_result.dart | 9 -- app/lib/common/models/userdata/userdata.dart | 14 --- app/lib/common/utilities/drug_utils.dart | 4 +- app/lib/common/utilities/genome_data.dart | 27 +----- app/lib/common/widgets/drug_list/cubit.dart | 2 +- app/lib/l10n/app_en.arb | 4 +- app/lib/login/cubit.dart | 91 ++++--------------- app/lib/login/models/lab.dart | 44 ++++++--- .../oauth_authorization_code_flow_lab.dart | 69 ++++++++++++++ app/lib/login/pages/login.dart | 11 ++- app/lib/main.dart | 2 +- 14 files changed, 137 insertions(+), 147 deletions(-) create mode 100644 app/lib/login/models/oauth_authorization_code_flow_lab.dart diff --git a/app/generate_screendocs/sequence_utils.dart b/app/generate_screendocs/sequence_utils.dart index 9376b4f5d..86babba45 100644 --- a/app/generate_screendocs/sequence_utils.dart +++ b/app/generate_screendocs/sequence_utils.dart @@ -8,7 +8,7 @@ import 'package:provider/provider.dart'; Future loadApp(WidgetTester tester) async { // Part before runApp in lib/main.dart await initServices(); - await updateGenotypeResults(); + await maybeUpdateGenotypeResults(); // Load the app await tester.pumpWidget( ChangeNotifierProvider( diff --git a/app/integration_test/login_test.dart b/app/integration_test/login_test.dart index e4b1a73e6..a75b8b640 100644 --- a/app/integration_test/login_test.dart +++ b/app/integration_test/login_test.dart @@ -1,5 +1,4 @@ import 'package:app/common/module.dart'; -import 'package:app/login/models/lab.dart'; import 'package:app/login/module.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/app/lib/common/constants.dart b/app/lib/common/constants.dart index 657448dcf..be7a46a22 100644 --- a/app/lib/common/constants.dart +++ b/app/lib/common/constants.dart @@ -2,10 +2,6 @@ import 'package:url_launcher/url_launcher.dart'; Uri anniUrl([String slug = '']) => Uri.http('vm-slosarek01.dhclab.i.hpi.de:8000', 'api/v1/$slug'); -Uri labServerUrl([String slug = '']) => - Uri.http('vm-slosarek01.dhclab.i.hpi.de:8081', 'api/v1/$slug'); -Uri keycloakUrl([String slug = '']) => - Uri.http('vm-slosarek01.dhclab.i.hpi.de:28080', slug); final geneticInformationUrl = Uri.https( 'medlineplus.gov', diff --git a/app/lib/common/models/userdata/lab_result.dart b/app/lib/common/models/userdata/lab_result.dart index 517542c8d..114aecae9 100644 --- a/app/lib/common/models/userdata/lab_result.dart +++ b/app/lib/common/models/userdata/lab_result.dart @@ -1,8 +1,5 @@ -import 'dart:convert'; - import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hive/hive.dart'; -import 'package:http/http.dart'; import 'genotype.dart'; part 'lab_result.g.dart'; @@ -35,9 +32,3 @@ class LabResult implements Genotype { @HiveField(3) String allelesTested; } - -// assumes http reponse from lab server -List labDataFromHTTPResponse(Response resp) { - final json = jsonDecode(resp.body)['diplotypes'] as List; - return json.map(LabResult.fromJson).toList(); -} diff --git a/app/lib/common/models/userdata/userdata.dart b/app/lib/common/models/userdata/userdata.dart index 1f8ede964..9fd7f1060 100644 --- a/app/lib/common/models/userdata/userdata.dart +++ b/app/lib/common/models/userdata/userdata.dart @@ -1,8 +1,4 @@ -import 'dart:convert'; - import 'package:hive/hive.dart'; -import 'package:http/http.dart'; - import '../../module.dart'; import '../../utilities/hive_utils.dart'; @@ -136,13 +132,3 @@ Future initUserData() async { final userData = Hive.box(_boxName); UserData._instance = userData.get('data') ?? UserData(); } - -// assumes http response from lab server -List activeDrugsFromHTTPResponse(Response resp) { - var activeDrugs = []; - final json = jsonDecode(resp.body) as Map; - if (json.containsKey('medications')) { - activeDrugs = List.from(json['medications']); - } - return activeDrugs; -} diff --git a/app/lib/common/utilities/drug_utils.dart b/app/lib/common/utilities/drug_utils.dart index 31aedd54e..e517193ac 100644 --- a/app/lib/common/utilities/drug_utils.dart +++ b/app/lib/common/utilities/drug_utils.dart @@ -4,7 +4,7 @@ import 'package:http/http.dart'; import '../../app.dart'; import '../module.dart'; -Future updateDrugsWithGuidelines() async { +Future maybeUpdateDrugsWithGuidelines() async { final isOnline = await hasConnectionTo(anniUrl().host); if (!isOnline && DrugsWithGuidelines.instance.version == null) { throw Exception(); @@ -22,7 +22,7 @@ Future updateDrugsWithGuidelines() async { DrugsWithGuidelines.instance.drugs = data.drugs; DrugsWithGuidelines.instance.version = data.version; await DrugsWithGuidelines.save(); - await updateGenotypeResults(); + await maybeUpdateGenotypeResults(); if (previousVersion != null) { final context = PharMeApp.navigatorKey.currentContext; if (context != null) { diff --git a/app/lib/common/utilities/genome_data.dart b/app/lib/common/utilities/genome_data.dart index b5a7d1c55..ff3b47e18 100644 --- a/app/lib/common/utilities/genome_data.dart +++ b/app/lib/common/utilities/genome_data.dart @@ -5,30 +5,11 @@ import 'package:http/http.dart'; import '../module.dart'; -Future fetchAndSaveDiplotypesAndActiveDrugs( - String token, String url, ActiveDrugs activeDrugs) async { - if (!shouldFetchDiplotypes()) return; - final response = await getDiplotypes(token, url); - if (response.statusCode == 200) { - await _saveDiplotypeAndActiveDrugsResponse(response, activeDrugs); - } else { - throw Exception(); - } -} - -Future getDiplotypes(String? token, String url) async { - return get(Uri.parse(url), headers: {'Authorization': 'Bearer $token'}); -} - -Future _saveDiplotypeAndActiveDrugsResponse( - Response response, +Future saveDiplotypesAndActiveDrugs( + List labData, + List activeDrugList, ActiveDrugs activeDrugs, ) async { - // parse response to list of user's labData - final labData = - labDataFromHTTPResponse(response); - final activeDrugList = activeDrugsFromHTTPResponse(response); - UserData.instance.labData = labData; await UserData.save(); await activeDrugs.setList(activeDrugList); @@ -63,7 +44,7 @@ Map initializeGenotypeResultKeys() { return emptyGenotypeResults; } -Future updateGenotypeResults() async { +Future maybeUpdateGenotypeResults() async { final skipUpdate = !shouldUpdateGenotypeResults(); if (skipUpdate) return; diff --git a/app/lib/common/widgets/drug_list/cubit.dart b/app/lib/common/widgets/drug_list/cubit.dart index bc3a3f984..4fe7cc620 100644 --- a/app/lib/common/widgets/drug_list/cubit.dart +++ b/app/lib/common/widgets/drug_list/cubit.dart @@ -62,7 +62,7 @@ class DrugListCubit extends Cubit { emit(DrugListState.loading()); try { - await updateDrugsWithGuidelines(); + await maybeUpdateDrugsWithGuidelines(); await loadDrugs(updateIfNull: false, filter: filter); } catch (error) { emit(DrugListState.error()); diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index a885fd2da..77aaf04f9 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -40,7 +40,7 @@ "drug_list_subheader_all_drugs": "All medications", "drug_list_subheader_other_drugs": "Other medications", - "err_could_not_retrieve_access_token": "An unexpected error occurred while retrieving the access token", + "err_could_not_retrieve_access_token": "An unexpected error occurred while logging in", "err_fetch_user_data_failed": "An error occurred while getting data, please try again later", "err_generic": "Error", @@ -137,7 +137,7 @@ } }, "drugs_page_tooltip_guideline_present": "{source} guidelines are used to inform the content on this page. These guidelines provide recommendations on which drugs to use based on your DNA.", - "@drugs_page_tooltip_guideline": { + "@drugs_page_tooltip_guideline_present": { "placeholders": { "source": { "type": "String", diff --git a/app/lib/login/cubit.dart b/app/lib/login/cubit.dart index b1cf8f1b1..8237a67b5 100644 --- a/app/lib/login/cubit.dart +++ b/app/lib/login/cubit.dart @@ -1,9 +1,4 @@ -import 'dart:convert' show jsonDecode; - -import 'package:flutter/services.dart'; -import 'package:flutter_web_auth/flutter_web_auth.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:http/http.dart' as http; import '../../common/module.dart'; import 'models/lab.dart'; @@ -21,23 +16,12 @@ class LoginCubit extends Cubit { // genomic data from it's endpoint. Future signInAndLoadUserData(BuildContext context, Lab lab) async { emit(LoginState.loadingUserData(null)); - - // authenticate - String? token; try { - token = await _getAccessToken( - context, - authUrl: lab.authUrl, - tokenUrl: lab.tokenUrl, - ); - } on PlatformException catch (e) { - if (e.code == 'CANCELED') { - revertToInitialState(); - return; - } - } - - if (token == null) { + await lab.authenticate(); + } on LabAuthenticationCanceled { + revertToInitialState(); + return; + } on LabAuthenticationError { emit(LoginState.error( // ignore: use_build_context_synchronously context.l10n.err_could_not_retrieve_access_token, @@ -46,24 +30,22 @@ class LoginCubit extends Cubit { } try { - final needNewDataLoad = - shouldFetchDiplotypes() || shouldUpdateGenotypeResults(); - // get data - if (needNewDataLoad) { + final loadingMessage = shouldFetchDiplotypes() // ignore: use_build_context_synchronously - emit(LoginState.loadingUserData(context.l10n.auth_loading_data)); - } - await fetchAndSaveDiplotypesAndActiveDrugs( - token, lab.starAllelesUrl.toString(), activeDrugs); - await updateGenotypeResults(); - - if (!needNewDataLoad) { + ? context.l10n.auth_loading_data // ignore: use_build_context_synchronously - emit(LoginState.loadingUserData(context.l10n.auth_updating_data)); + : context.l10n.auth_updating_data; + emit(LoginState.loadingUserData(loadingMessage)); + if (shouldFetchDiplotypes()) { + final (labData, activeDrugList) = await lab.loadData(); + await saveDiplotypesAndActiveDrugs( + labData, + activeDrugList, + activeDrugs, + ); } - await updateDrugsWithGuidelines(); - - // login + fetching of data successful + await maybeUpdateGenotypeResults(); + await maybeUpdateDrugsWithGuidelines(); MetaData.instance.isLoggedIn = true; await MetaData.save(); emit(LoginState.loadedUserData()); @@ -72,43 +54,6 @@ class LoginCubit extends Cubit { emit(LoginState.error(context.l10n.err_fetch_user_data_failed)); } } - - Future _getAccessToken( - BuildContext context, { - required Uri authUrl, - required Uri tokenUrl, - }) async { - const clientId = 'pharme-app'; - const callbackUrlScheme = 'localhost'; - - // Construct the url - final url = authUrl.replace(queryParameters: { - 'response_type': 'code', - 'client_id': clientId, - 'redirect_uri': '$callbackUrlScheme:/', - 'scope': 'openid profile', - }); - - // Present the dialog to the user - final result = await FlutterWebAuth.authenticate( - url: url.toString(), - callbackUrlScheme: callbackUrlScheme, - ); - - // Extract code from resulting url - final code = Uri.parse(result).queryParameters['code']; - - // Use this code to get an access token - final response = await http.post(tokenUrl, body: { - 'client_id': clientId, - 'redirect_uri': '$callbackUrlScheme:/', - 'grant_type': 'authorization_code', - 'code': code, - }); - - // Get the access token from the response - return jsonDecode(response.body)['access_token'] as String; - } } @freezed diff --git a/app/lib/login/models/lab.dart b/app/lib/login/models/lab.dart index 42fc144ae..29c4102ba 100644 --- a/app/lib/login/models/lab.dart +++ b/app/lib/login/models/lab.dart @@ -1,24 +1,38 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; + import '../../common/module.dart'; +class LabAuthenticationCanceled implements Exception { + LabAuthenticationCanceled(); +} + +class LabAuthenticationError implements Exception { + LabAuthenticationError(); +} + class Lab { Lab({ required this.name, - required this.authUrl, - required this.tokenUrl, - required this.starAllelesUrl, }); String name; - Uri authUrl; - Uri tokenUrl; - Uri starAllelesUrl; -} + + Future authenticate() async {} + Future<(List, List)> loadData() async { + throw UnimplementedError(); + } -final labs = [ - Lab( - name: 'Mount Sinai Health System', - authUrl: keycloakUrl('realms/pharme/protocol/openid-connect/auth'), - tokenUrl: keycloakUrl('realms/pharme/protocol/openid-connect/token'), - starAllelesUrl: labServerUrl('star-alleles'), - ) -]; + (List, List) labDataFromHTTPResponse(Response response) { + final json = jsonDecode(response.body) as Map; + final labData = json['diplotypes'].map( + LabResult.fromJson + ).toList(); + var activeDrugs = []; + if (json.containsKey('medications')) { + activeDrugs = List.from(json['medications']); + } + return (labData, activeDrugs); + } +} diff --git a/app/lib/login/models/oauth_authorization_code_flow_lab.dart b/app/lib/login/models/oauth_authorization_code_flow_lab.dart new file mode 100644 index 000000000..8b954edbc --- /dev/null +++ b/app/lib/login/models/oauth_authorization_code_flow_lab.dart @@ -0,0 +1,69 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart'; +import 'package:flutter_web_auth/flutter_web_auth.dart'; +import 'package:http/http.dart' as http; + +import '../../common/module.dart'; +import 'lab.dart'; + +class OAuthAuthorizationCodeFlowLab extends Lab { + OAuthAuthorizationCodeFlowLab({ + required super.name, + required this.authUrl, + required this.tokenUrl, + required this.dataUrl, + }); + + Uri authUrl; + Uri tokenUrl; + Uri dataUrl; + + late String? token; + + @override + Future authenticate() async { + const clientId = 'pharme-app'; + const callbackUrlScheme = 'localhost'; + final url = authUrl.replace(queryParameters: { + 'response_type': 'code', + 'client_id': clientId, + 'redirect_uri': '$callbackUrlScheme:/', + 'scope': 'openid profile', + }); + try { + final result = await FlutterWebAuth.authenticate( + url: url.toString(), + callbackUrlScheme: callbackUrlScheme, + ); + final code = Uri.parse(result).queryParameters['code']; + final response = await http.post(tokenUrl, body: { + 'client_id': clientId, + 'redirect_uri': '$callbackUrlScheme:/', + 'grant_type': 'authorization_code', + 'code': code, + }); + token = jsonDecode(response.body)['access_token'] as String; + } on PlatformException catch (e) { + if (e.code == 'CANCELED') { + throw LabAuthenticationCanceled(); + } + } + if (token == null) { + throw LabAuthenticationError(); + } + } + + @override + Future<(List, List)> loadData() async { + final response = await http.get( + dataUrl, + headers: {'Authorization': 'Bearer $token'}, + ); + if (response.statusCode == 200) { + return labDataFromHTTPResponse(response); + } else { + throw Exception(); + } + } +} \ No newline at end of file diff --git a/app/lib/login/pages/login.dart b/app/lib/login/pages/login.dart index da8a6a8b4..748a5a921 100644 --- a/app/lib/login/pages/login.dart +++ b/app/lib/login/pages/login.dart @@ -3,7 +3,16 @@ import 'package:provider/provider.dart'; import '../../../common/module.dart'; import '../cubit.dart'; -import '../models/lab.dart'; +import '../models/oauth_authorization_code_flow_lab.dart'; + +final labs = [ + OAuthAuthorizationCodeFlowLab( + name: 'Mount Sinai Health System', + authUrl: Uri.http('vm-slosarek01.dhclab.i.hpi.de:28080', 'realms/pharme/protocol/openid-connect/auth'), + tokenUrl: Uri.http('vm-slosarek01.dhclab.i.hpi.de:28080', 'realms/pharme/protocol/openid-connect/token'), + dataUrl: Uri.http('vm-slosarek01.dhclab.i.hpi.de:8081', 'api/v1/star-alleles'), + ) +]; @RoutePage() class LoginPage extends HookWidget { diff --git a/app/lib/main.dart b/app/lib/main.dart index aa70d2bb1..cca53c5fc 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -6,7 +6,7 @@ import 'common/module.dart'; Future main() async { await initServices(); // Maybe refresh lookups on app start - await updateGenotypeResults(); + await maybeUpdateGenotypeResults(); runApp( ChangeNotifierProvider( create: (context) => ActiveDrugs(),