From 95e78a93ad71c2efb5e720f5cd9dbceff9d3366f Mon Sep 17 00:00:00 2001 From: bardram Date: Wed, 18 Dec 2024 14:14:24 +0100 Subject: [PATCH] fix of #457 --- backends/carp_webservices/CHANGELOG.md | 4 + backends/carp_webservices/example/.metadata | 25 ++---- .../example/analysis_options.yaml | 28 +++++++ .../lib/carp_auth/carp_auth.g.dart | 65 +++++---------- .../lib/carp_auth/carp_auth_service.dart | 83 ++++++++++--------- .../lib/carp_auth/carp_user.dart | 22 ++--- .../lib/carp_services/carp_services.g.dart | 57 +++++-------- backends/carp_webservices/pubspec.yaml | 2 +- 8 files changed, 136 insertions(+), 150 deletions(-) create mode 100644 backends/carp_webservices/example/analysis_options.yaml diff --git a/backends/carp_webservices/CHANGELOG.md b/backends/carp_webservices/CHANGELOG.md index ad7673c4..bc0c36ec 100644 --- a/backends/carp_webservices/CHANGELOG.md +++ b/backends/carp_webservices/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.5.2 + +* fix of issues [#457](https://github.com/cph-cachet/carp.sensing-flutter/issues/457) + ## 3.5.1 * upgrade to carp_serialization v. 2.0 & carp_mobile_sensing: 1.10.0 diff --git a/backends/carp_webservices/example/.metadata b/backends/carp_webservices/example/.metadata index bae49f35..edbc7fba 100644 --- a/backends/carp_webservices/example/.metadata +++ b/backends/carp_webservices/example/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "7482962148e8d758338d8a28f589f317e1e42ba4" + revision: "5874a72aa4c779a02553007c47dacbefba2374dc" channel: "stable" project_type: app @@ -13,26 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 - base_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 - - platform: android - create_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 - base_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 - - platform: ios - create_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 - base_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 - - platform: linux - create_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 - base_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 + create_revision: 5874a72aa4c779a02553007c47dacbefba2374dc + base_revision: 5874a72aa4c779a02553007c47dacbefba2374dc - platform: macos - create_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 - base_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 - - platform: web - create_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 - base_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 - - platform: windows - create_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 - base_revision: 7482962148e8d758338d8a28f589f317e1e42ba4 + create_revision: 5874a72aa4c779a02553007c47dacbefba2374dc + base_revision: 5874a72aa4c779a02553007c47dacbefba2374dc # User provided section diff --git a/backends/carp_webservices/example/analysis_options.yaml b/backends/carp_webservices/example/analysis_options.yaml new file mode 100644 index 00000000..0d290213 --- /dev/null +++ b/backends/carp_webservices/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/backends/carp_webservices/lib/carp_auth/carp_auth.g.dart b/backends/carp_webservices/lib/carp_auth/carp_auth.g.dart index 61d6da6c..999da935 100644 --- a/backends/carp_webservices/lib/carp_auth/carp_auth.g.dart +++ b/backends/carp_webservices/lib/carp_auth/carp_auth.g.dart @@ -15,54 +15,35 @@ OAuthToken _$OAuthTokenFromJson(Map json) => OAuthToken( json['id_token'] as String, )..expiresIn = (json['expires_in'] as num?)?.toInt(); -Map _$OAuthTokenToJson(OAuthToken instance) { - final val = { - 'access_token': instance.accessToken, - 'refresh_token': instance.refreshToken, - 'token_type': instance.tokenType, - 'id_token': instance.idToken, - 'scope': instance.scope, - 'expires_at': instance.expiresAt.toIso8601String(), - }; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('expires_in', instance.expiresIn); - return val; -} +Map _$OAuthTokenToJson(OAuthToken instance) => + { + 'access_token': instance.accessToken, + 'refresh_token': instance.refreshToken, + 'token_type': instance.tokenType, + 'id_token': instance.idToken, + 'scope': instance.scope, + 'expires_at': instance.expiresAt.toIso8601String(), + if (instance.expiresIn case final value?) 'expires_in': value, + }; CarpUser _$CarpUserFromJson(Map json) => CarpUser( username: json['username'] as String, id: json['id'] as String, - firstName: json['first_name'] as String, - lastName: json['last_name'] as String, - email: json['email'] as String, - roles: json['roles'] as List, + firstName: json['first_name'] as String?, + lastName: json['last_name'] as String?, + email: json['email'] as String?, + roles: json['roles'] as List? ?? const [], token: json['token'] == null ? null : OAuthToken.fromJson(json['token'] as Map), ); -Map _$CarpUserToJson(CarpUser instance) { - final val = { - 'username': instance.username, - 'id': instance.id, - 'email': instance.email, - 'first_name': instance.firstName, - 'last_name': instance.lastName, - 'roles': instance.roles, - }; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('token', instance.token?.toJson()); - return val; -} +Map _$CarpUserToJson(CarpUser instance) => { + 'username': instance.username, + 'id': instance.id, + if (instance.email case final value?) 'email': value, + if (instance.firstName case final value?) 'first_name': value, + if (instance.lastName case final value?) 'last_name': value, + 'roles': instance.roles, + if (instance.token?.toJson() case final value?) 'token': value, + }; diff --git a/backends/carp_webservices/lib/carp_auth/carp_auth_service.dart b/backends/carp_webservices/lib/carp_auth/carp_auth_service.dart index 91dacab5..edfcde25 100644 --- a/backends/carp_webservices/lib/carp_auth/carp_auth_service.dart +++ b/backends/carp_webservices/lib/carp_auth/carp_auth_service.dart @@ -117,7 +117,11 @@ class CarpAuthService { _manager!.userChanges().listen((user) { if (user != null) { _currentUser = getCurrentUserProfile(user); - _authEventController.add(AuthEvent.authenticated); + if (_currentUser != null) { + _authEventController.add(AuthEvent.authenticated); + } else { + _authEventController.add(AuthEvent.failed); + } } }); } @@ -128,28 +132,27 @@ class CarpAuthService { /// The discovery URL in the [authProperties] is used to find the Identity Server. /// /// Returns the signed in user (with an [OAuthToken] access token), if successful. - /// Throws a [CarpServiceException] if not successful. + /// Throws a [CarpServiceException] if not successful. Future authenticate() async { assert(_manager != null, 'Manager not configured. Call configure() first.'); - - if (!_manager!.didInit) { - await initManager(); - } + if (!_manager!.didInit) await initManager(); OidcUser? response = await manager!.loginAuthorizationCodeFlow(); if (response != null) { _currentUser = getCurrentUserProfile(response); - currentUser.authenticated(OAuthToken.fromTokenResponse(response.token)); - _authEventController.add(AuthEvent.authenticated); - return currentUser; + + if (_currentUser != null) { + _currentUser! + .authenticated(OAuthToken.fromTokenResponse(response.token)); + _authEventController.add(AuthEvent.authenticated); + return currentUser; + } } - // All other cases are treated as a failed attempt and throws an error + // All other cases are treated as a failed attempt _authEventController.add(AuthEvent.failed); - _currentUser = null; - // auth error response from CARP is in the form throw CarpServiceException( httpStatus: HTTPStatus(401), message: 'Authentication failed.', @@ -167,10 +170,7 @@ class CarpAuthService { required String password, }) async { assert(_manager != null, 'Manager not configured. Call configure() first.'); - - if (!_manager!.didInit) { - await initManager(); - } + if (!_manager!.didInit) await initManager(); final OidcUser? response = await manager?.loginPassword( username: username, @@ -179,17 +179,18 @@ class CarpAuthService { if (response != null) { _currentUser = getCurrentUserProfile(response); - currentUser.authenticated(OAuthToken.fromTokenResponse(response.token)); - _authEventController.add(AuthEvent.refreshed); - return currentUser; + + if (_currentUser != null) { + _currentUser! + .authenticated(OAuthToken.fromTokenResponse(response.token)); + _authEventController.add(AuthEvent.authenticated); + return currentUser; + } } - // All other cases are treated as a failed attempt and throws an error + // All other cases are treated as a failed attempt _authEventController.add(AuthEvent.failed); - _currentUser = null; - // auth error response from CARP is on the form - // {error: invalid_grant, error_description: Bad credentials} throw CarpServiceException( httpStatus: HTTPStatus(401), message: 'Authentication failed.', @@ -207,30 +208,27 @@ class CarpAuthService { /// Throws a [CarpServiceException] if not successful. Future refresh() async { assert(_manager != null, 'Manager not configured. Call configure() first.'); - - if (!_manager!.didInit) { - await initManager(); - } + if (!_manager!.didInit) await initManager(); final OidcUser? response = await manager?.refreshToken(); - print('reposnse : $response'); - if (response != null) { - currentUser = getCurrentUserProfile(response); - currentUser.authenticated(OAuthToken.fromTokenResponse(response.token)); - _authEventController.add(AuthEvent.refreshed); - return currentUser; + _currentUser = getCurrentUserProfile(response); + + if (_currentUser != null) { + _currentUser! + .authenticated(OAuthToken.fromTokenResponse(response.token)); + _authEventController.add(AuthEvent.authenticated); + return currentUser; + } } - // All other cases are treated as a failed attempt and throws an error + // All other cases are treated as a failed attempt _authEventController.add(AuthEvent.failed); - // auth error response from CARP is on the form - // {error: invalid_grant, error_description: Bad credentials} throw CarpServiceException( httpStatus: HTTPStatus(401), - message: 'Token refresh failed.', + message: 'Authentication failed.', ); } @@ -265,10 +263,13 @@ class CarpAuthService { // USERS // -------------------------------------------------------------------------- - /// Gets the CARP profile of the current user from the JWT token - CarpUser getCurrentUserProfile(OidcUser response) { - var jwt = JwtDecoder.decode(response.token.accessToken!); - return CarpUser.fromJWT(jwt, response.token); + /// Gets the CARP profile of the current user from the JWT token of [user]. + /// Returns null if the the [user] don't have an access token. + CarpUser? getCurrentUserProfile(OidcUser user) { + if (user.token.accessToken == null) return null; + + var jwt = JwtDecoder.decode(user.token.accessToken!); + return CarpUser.fromJWT(jwt, user.token); } /// Makes sure that the [CarpApp] or [CarpUser] is configured, by throwing a diff --git a/backends/carp_webservices/lib/carp_auth/carp_user.dart b/backends/carp_webservices/lib/carp_auth/carp_user.dart index e94684ed..100d7039 100644 --- a/backends/carp_webservices/lib/carp_auth/carp_user.dart +++ b/backends/carp_webservices/lib/carp_auth/carp_user.dart @@ -14,13 +14,13 @@ class CarpUser { String id; /// The user's email - String email; + String? email; /// User's first name - String firstName; + String? firstName; /// User's last name - String lastName; + String? lastName; /// The list of roles that this user has in CARP. List roles = []; @@ -32,10 +32,10 @@ class CarpUser { CarpUser({ required this.username, required this.id, - required this.firstName, - required this.lastName, - required this.email, - required this.roles, + this.firstName, + this.lastName, + this.email, + this.roles = const [], this.token, }); @@ -57,10 +57,10 @@ class CarpUser { return CarpUser( username: jwt['preferred_username'] as String, id: jwt['sub'] as String, - firstName: jwt['given_name'] as String, - lastName: jwt['family_name'] as String, - email: jwt['email'] as String, - roles: jwt['realm_access']['roles'] as List, + firstName: jwt['given_name'] as String?, + lastName: jwt['family_name'] as String?, + email: jwt['email'] as String?, + roles: jwt['realm_access']['roles'] as List? ?? [], token: OAuthToken.fromTokenResponse(token), ); } diff --git a/backends/carp_webservices/lib/carp_services/carp_services.g.dart b/backends/carp_webservices/lib/carp_services/carp_services.g.dart index c47c29e1..13371735 100644 --- a/backends/carp_webservices/lib/carp_services/carp_services.g.dart +++ b/backends/carp_webservices/lib/carp_services/carp_services.g.dart @@ -14,22 +14,14 @@ DataPoint _$DataPointFromJson(Map json) => DataPoint( ..studyId = json['study_id'] as String? ..carpBody = json['carp_body'] as Map?; -Map _$DataPointToJson(DataPoint instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('id', instance.id); - writeNotNull('created_by_user_id', instance.createdByUserId); - writeNotNull('study_id', instance.studyId); - val['carp_header'] = instance.carpHeader.toJson(); - writeNotNull('carp_body', instance.carpBody); - return val; -} +Map _$DataPointToJson(DataPoint instance) => { + if (instance.id case final value?) 'id': value, + if (instance.createdByUserId case final value?) + 'created_by_user_id': value, + if (instance.studyId case final value?) 'study_id': value, + 'carp_header': instance.carpHeader.toJson(), + if (instance.carpBody case final value?) 'carp_body': value, + }; DataPointHeader _$DataPointHeaderFromJson(Map json) => DataPointHeader( @@ -50,22 +42,17 @@ DataPointHeader _$DataPointHeaderFromJson(Map json) => ? null : DateTime.parse(json['upload_time'] as String); -Map _$DataPointHeaderToJson(DataPointHeader instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('study_id', instance.studyId); - writeNotNull('device_role_name', instance.deviceRoleName); - writeNotNull('trigger_id', instance.triggerId); - writeNotNull('user_id', instance.userId); - writeNotNull('upload_time', instance.uploadTime?.toIso8601String()); - writeNotNull('start_time', instance.startTime?.toIso8601String()); - writeNotNull('end_time', instance.endTime?.toIso8601String()); - writeNotNull('data_format', instance.dataFormat?.toJson()); - return val; -} +Map _$DataPointHeaderToJson(DataPointHeader instance) => + { + if (instance.studyId case final value?) 'study_id': value, + if (instance.deviceRoleName case final value?) 'device_role_name': value, + if (instance.triggerId case final value?) 'trigger_id': value, + if (instance.userId case final value?) 'user_id': value, + if (instance.uploadTime?.toIso8601String() case final value?) + 'upload_time': value, + if (instance.startTime?.toIso8601String() case final value?) + 'start_time': value, + if (instance.endTime?.toIso8601String() case final value?) + 'end_time': value, + if (instance.dataFormat?.toJson() case final value?) 'data_format': value, + }; diff --git a/backends/carp_webservices/pubspec.yaml b/backends/carp_webservices/pubspec.yaml index 5d11b098..58456be0 100644 --- a/backends/carp_webservices/pubspec.yaml +++ b/backends/carp_webservices/pubspec.yaml @@ -1,6 +1,6 @@ name: carp_webservices description: Flutter API for accessing the CARP web services - authentication, file management, data points, and app-specific collections of documents. -version: 3.5.1 +version: 3.5.2 homepage: https://github.com/cph-cachet/carp.sensing-flutter/tree/master/backends/carp_webservices environment: