-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathuser_repository.dart
335 lines (287 loc) · 11.6 KB
/
user_repository.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
// Dart imports:
import 'dart:convert';
// Flutter imports:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// Package imports:
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:logger/logger.dart';
// Project imports:
import 'package:notredame/features/app/analytics/analytics_service.dart';
import 'package:notredame/features/app/integration/networking_service.dart';
import 'package:notredame/features/app/monets_api/models/mon_ets_user.dart';
import 'package:notredame/features/app/monets_api/monets_api_client.dart';
import 'package:notredame/features/app/signets-api/models/profile_student.dart';
import 'package:notredame/features/app/signets-api/models/program.dart';
import 'package:notredame/features/app/signets-api/signets_api_client.dart';
import 'package:notredame/features/app/storage/cache_manager.dart';
import 'package:notredame/utils/api_exception.dart';
import 'package:notredame/utils/cache_exception.dart';
import 'package:notredame/utils/http_exception.dart';
import 'package:notredame/utils/locator.dart';
class UserRepository {
static const String tag = "UserRepository";
static const String usernameSecureKey = "usernameKey";
static const String passwordSecureKey = "passwordKey";
@visibleForTesting
static const String infoCacheKey = "infoCache";
@visibleForTesting
static const String programsCacheKey = "programsCache";
final Logger _logger = locator<Logger>();
/// Will be used to report event and error.
final AnalyticsService _analyticsService = locator<AnalyticsService>();
/// Used to verify if the user has connectivity
final NetworkingService _networkingService = locator<NetworkingService>();
/// Secure storage manager to access and update the cache.
final FlutterSecureStorage _secureStorage = locator<FlutterSecureStorage>();
/// Cache manager to access and update the cache.
final CacheManager _cacheManager = locator<CacheManager>();
/// Used to access the Signets API
final SignetsAPIClient _signetsApiClient = locator<SignetsAPIClient>();
/// Used to access the MonÉTS API
final MonETSAPIClient _monEtsApiClient = locator<MonETSAPIClient>();
/// Mon ETS user for the student
MonETSUser? _monETSUser;
MonETSUser? get monETSUser => _monETSUser;
/// Information for the student profile
ProfileStudent? _info;
ProfileStudent? get info => _info;
/// List of the programs for the student
List<Program>? _programs;
List<Program>? get programs => _programs;
/// Authenticate the user using the [username] (for a student should be the
/// universal code like AAXXXXX).
/// If the authentication is successful the credentials ([username] and [password])
/// will be saved in the secure storage of the device to authorize a silent
/// authentication next time.
Future<bool> authenticate(
{required String username,
required String password,
bool isSilent = false}) async {
try {
_monETSUser = await _monEtsApiClient.authenticate(
username: username, password: password);
} on Exception catch (e, stacktrace) {
// Try login in from signets if monETS failed
if (e is HttpException) {
try {
// ignore: deprecated_member_use
if (await _signetsApiClient.authenticate(
username: username, password: password)) {
_monETSUser = MonETSUser(
domain: MonETSUser.mainDomain,
typeUsagerId: MonETSUser.studentRoleId,
username: username);
} else {
_analyticsService.logError(tag, "Authenticate - $e", e, stacktrace);
return false;
}
} on Exception catch (e, stacktrace) {
_analyticsService.logError(tag, "Authenticate - $e", e, stacktrace);
return false;
}
} else {
_analyticsService.logError(tag, "Authenticate - $e", e, stacktrace);
return false;
}
}
await _analyticsService.setUserProperties(
userId: username, domain: _monETSUser!.domain);
// Save the credentials in the secure storage
if (!isSilent) {
try {
await _secureStorage.write(key: usernameSecureKey, value: username);
await _secureStorage.write(key: passwordSecureKey, value: password);
} on PlatformException catch (e, stacktrace) {
await _secureStorage.deleteAll();
_analyticsService.logError(
tag, "Authenticate - PlatformException - $e", e, stacktrace);
return false;
}
}
return true;
}
/// Check if there are credentials saved and so authenticate the user, otherwise
/// return false
Future<bool> silentAuthenticate() async {
try {
final username = await _secureStorage.read(key: usernameSecureKey);
if (username != null) {
final password = await _secureStorage.read(key: passwordSecureKey);
if (password == null) {
await _secureStorage.deleteAll();
_analyticsService.logError(tag,
"SilentAuthenticate - PlatformException(Handled) - $passwordSecureKey not found");
return false;
}
return await authenticate(
username: username, password: password, isSilent: true);
}
} on PlatformException catch (e, stacktrace) {
await _secureStorage.deleteAll();
_analyticsService.logError(
tag,
"SilentAuthenticate - PlatformException(Handled) - $e",
e,
stacktrace);
}
return false;
}
/// Log out the user
Future<bool> logOut() async {
_monETSUser = null;
// Delete the credentials from the secure storage
try {
await _secureStorage.delete(key: usernameSecureKey);
await _secureStorage.delete(key: passwordSecureKey);
} on PlatformException catch (e, stacktrace) {
await _secureStorage.deleteAll();
_analyticsService.logError(
tag, "Authenticate - PlatformException - $e", e, stacktrace);
return false;
}
return true;
}
/// Retrieve and get the password for the current authenticated user.
/// WARNING This isn't a good practice but currently the password has to be sent in clear.
Future<String> getPassword() async {
if (_monETSUser == null) {
_analyticsService.logEvent(
tag, "Trying to acquire password but not authenticated");
final result = await silentAuthenticate();
if (!result) {
throw const ApiException(prefix: tag, message: "Not authenticated");
}
}
try {
final password = await _secureStorage.read(key: passwordSecureKey);
if (password == null) {
_analyticsService.logEvent(
tag, "Trying to acquire password but not authenticated");
throw const ApiException(prefix: tag, message: "Not authenticated");
}
return password;
} on PlatformException catch (e, stacktrace) {
await _secureStorage.deleteAll();
_analyticsService.logError(
tag, "getPassword - PlatformException - $e", e, stacktrace);
throw const ApiException(prefix: tag, message: "Not authenticated");
}
}
/// Get the list of programs on which the student was active.
/// The list from the [CacheManager] is loaded than updated with the results
/// from the [SignetsApi].
Future<List<Program>> getPrograms({bool fromCacheOnly = false}) async {
// Force fromCacheOnly mode when user has no connectivity
if (!(await _networkingService.hasConnectivity())) {
// ignore: parameter_assignments
fromCacheOnly = !await _networkingService.hasConnectivity();
}
// Load the programs from the cache if the list doesn't exist
if (_programs == null) {
try {
_programs = [];
final List programsCached =
jsonDecode(await _cacheManager.get(programsCacheKey))
as List<dynamic>;
// Build list of programs loaded from the cache.
_programs = programsCached
.map((e) => Program.fromJson(e as Map<String, dynamic>))
.toList();
_logger.d(
"$tag - getPrograms: ${_programs!.length} programs loaded from cache.");
} on CacheException catch (_) {
_logger.e(
"$tag - getPrograms: exception raised while trying to load the programs from cache.");
}
}
if (fromCacheOnly) {
return _programs!;
}
try {
// getPassword will try to authenticate the user if not authenticated.
final String password = await getPassword();
if (_monETSUser != null) {
_programs = await _signetsApiClient.getPrograms(
username: _monETSUser!.universalCode, password: password);
_logger.d("$tag - getPrograms: ${_programs!.length} programs fetched.");
// Update cache
_cacheManager.update(programsCacheKey, jsonEncode(_programs));
}
} on CacheException catch (_) {
_logger.e(
"$tag - getPrograms: exception raised while trying to update the cache.");
return _programs!;
} on Exception catch (e, stacktrace) {
_analyticsService.logError(
tag, "Exception raised during getPrograms: $e", e, stacktrace);
rethrow;
}
return _programs!;
}
/// Get the profile information.
/// The information from the [CacheManager] is loaded than updated with the results
/// from the [SignetsApi].
Future<ProfileStudent?> getInfo({bool fromCacheOnly = false}) async {
// Force fromCacheOnly mode when user has no connectivity
if (!(await _networkingService.hasConnectivity())) {
// ignore: parameter_assignments
fromCacheOnly = true;
}
// Load the student profile from the cache if the information doesn't exist
if (_info == null) {
try {
final infoCached = jsonDecode(await _cacheManager.get(infoCacheKey))
as Map<String, dynamic>;
// Build info loaded from the cache.
_info = ProfileStudent.fromJson(infoCached);
_logger.d("$tag - getInfo: $_info info loaded from cache.");
} on CacheException catch (e) {
_logger.e(
"$tag - getInfo: exception raised while trying to load the info from cache.",
error: e);
}
}
if (fromCacheOnly) {
return _info;
}
try {
// getPassword will try to authenticate the user if not authenticated.
final String password = await getPassword();
if (_monETSUser != null) {
final fetchedInfo = await _signetsApiClient.getStudentInfo(
username: _monETSUser!.universalCode, password: password);
_logger.d("$tag - getInfo: $fetchedInfo info fetched.");
if (_info != fetchedInfo) {
_info = fetchedInfo;
// Update cache
_cacheManager.update(infoCacheKey, jsonEncode(_info));
}
}
} on CacheException catch (_) {
_logger.e(
"$tag - getInfo: exception raised while trying to update the cache.");
return _info!;
} on Exception catch (e, stacktrace) {
_analyticsService.logError(
tag, "Exception raised during getInfo: $e", e, stacktrace);
rethrow;
}
return _info!;
}
/// Check whether the user was previously authenticated.
Future<bool> wasPreviouslyLoggedIn() async {
try {
final username = await _secureStorage.read(key: passwordSecureKey);
if (username != null) {
final password = await _secureStorage.read(key: passwordSecureKey);
return password != null && password.isNotEmpty;
}
} on PlatformException catch (e, stacktrace) {
await _secureStorage.deleteAll();
_analyticsService.logError(
tag, "getPassword - PlatformException - $e", e, stacktrace);
}
return false;
}
}