From 32d8333c7caf8ff74db1ec7a1b32854ad2a6c50e Mon Sep 17 00:00:00 2001 From: Nutcake Date: Tue, 9 May 2023 10:49:20 +0200 Subject: [PATCH] Add support for email login and improve general error handling --- lib/apis/user_api.dart | 4 +-- lib/auxiliary.dart | 45 ++++++------------------- lib/clients/api_client.dart | 28 +++++++++++---- lib/clients/settings_client.dart | 15 +++++++-- lib/models/settings.dart | 18 +++++----- lib/models/user_profile.dart | 4 +-- lib/widgets/friends/user_list_tile.dart | 2 +- lib/widgets/friends/user_search.dart | 6 ++-- 8 files changed, 63 insertions(+), 59 deletions(-) diff --git a/lib/apis/user_api.dart b/lib/apis/user_api.dart index 5424579..a89bbe4 100644 --- a/lib/apis/user_api.dart +++ b/lib/apis/user_api.dart @@ -29,8 +29,8 @@ class UserApi { return UserStatus.fromMap(data); } - static Future notifyOnlineInstance(ApiClient client) async { - final response = await client.post("/stats/instanceOnline/${client.authenticationData.secretMachineId.hashCode}"); + static Future notifyOnlineInstance(ApiClient client, {required String machineId}) async { + final response = await client.post("/stats/instanceOnline/$machineId"); ApiClient.checkResponse(response); } diff --git a/lib/auxiliary.dart b/lib/auxiliary.dart index 6d1f223..027f341 100644 --- a/lib/auxiliary.dart +++ b/lib/auxiliary.dart @@ -1,6 +1,9 @@ +import 'dart:convert'; +import 'dart:typed_data'; + import 'package:contacts_plus_plus/config.dart'; -import 'package:path/path.dart' as p; import 'package:html/parser.dart' as htmlparser; +import 'package:uuid/uuid.dart'; enum NeosDBEndpoint { @@ -10,39 +13,6 @@ enum NeosDBEndpoint videoCDN, } -extension NeosStringExtensions on Uri { - static String dbSignature(Uri neosdb) => neosdb.pathSegments.length < 2 ? "" : p.basenameWithoutExtension(neosdb.pathSegments[1]); - static String? neosDBQuery(Uri neosdb) => neosdb.query.trim().isEmpty ? null : neosdb.query.substring(1); - static bool isLegacyNeosDB(Uri uri) => !(uri.scheme != "neosdb") && uri.pathSegments.length >= 2 && p.basenameWithoutExtension(uri.pathSegments[1]).length < 30; - - Uri neosDBToHTTP(NeosDBEndpoint endpoint) { - var signature = dbSignature(this); - var query = neosDBQuery(this); - if (query != null) { - signature = "$signature/$query"; - } - if (isLegacyNeosDB(this)) { - return Uri.parse(Config.legacyCloudUrl + signature); - } - String base; - switch (endpoint) { - case NeosDBEndpoint.blob: - base = Config.blobStorageUrl; - break; - case NeosDBEndpoint.cdn: - base = Config.neosCdnUrl; - break; - case NeosDBEndpoint.videoCDN: - base = Config.videoStorageUrl; - break; - case NeosDBEndpoint.def: - base = Config.neosAssetsUrl; - } - - return Uri.parse(base + signature); - } -} - class Aux { static String neosDbToHttp(String? neosdb) { if (neosdb == null || neosdb.isEmpty) return ""; @@ -55,6 +25,13 @@ class Aux { } return fullUri; } + + static String toURLBase64(Uint8List data) => base64.encode(data) + .replaceAll("+", "-") + .replaceAll("/", "_") + .replaceAll("=", ""); + + static String generateMachineId() => Aux.toURLBase64((const Uuid().v1obj().toBytes())).toLowerCase(); } diff --git a/lib/clients/api_client.dart b/lib/clients/api_client.dart index 825281c..e52f86e 100644 --- a/lib/clients/api_client.dart +++ b/lib/clients/api_client.dart @@ -13,11 +13,11 @@ import '../config.dart'; class ApiClient { static const String totpKey = "TOTP"; static const String userIdKey = "userId"; - static const String machineIdKey = "machineId"; + static const String secretMachineIdKey = "machineId"; static const String tokenKey = "token"; static const String passwordKey = "password"; - ApiClient({required AuthenticationData authenticationData}) : _authenticationData = authenticationData; + const ApiClient({required AuthenticationData authenticationData}) : _authenticationData = authenticationData; final AuthenticationData _authenticationData; @@ -33,7 +33,7 @@ class ApiClient { String? oneTimePad, }) async { final body = { - "username": username, + (username.contains("@") ? "email" : "username"): username.trim(), "password": password, "rememberMe": rememberMe, "secretMachineId": const Uuid().v4(), @@ -58,7 +58,7 @@ class ApiClient { if (authData.isAuthenticated) { const FlutterSecureStorage storage = FlutterSecureStorage(); await storage.write(key: userIdKey, value: authData.userId); - await storage.write(key: machineIdKey, value: authData.secretMachineId); + await storage.write(key: secretMachineIdKey, value: authData.secretMachineId); await storage.write(key: tokenKey, value: authData.token); if (rememberPass) await storage.write(key: passwordKey, value: password); } @@ -68,7 +68,7 @@ class ApiClient { static Future tryCachedLogin() async { const FlutterSecureStorage storage = FlutterSecureStorage(); String? userId = await storage.read(key: userIdKey); - String? machineId = await storage.read(key: machineIdKey); + String? machineId = await storage.read(key: secretMachineIdKey); String? token = await storage.read(key: tokenKey); String? password = await storage.read(key: passwordKey); @@ -97,10 +97,17 @@ class ApiClient { return AuthenticationData.unauthenticated(); } + Future extendSession() async { + final response = await patch("/userSessions"); + if (response.statusCode != 204) { + throw "Failed to extend session."; + } + } + Future logout(BuildContext context) async { const FlutterSecureStorage storage = FlutterSecureStorage(); await storage.delete(key: userIdKey); - await storage.delete(key: machineIdKey); + await storage.delete(key: secretMachineIdKey); await storage.delete(key: tokenKey); await storage.delete(key: passwordKey); if (context.mounted) { @@ -117,7 +124,8 @@ class ApiClient { // TODO: Show the login screen again if cached login was unsuccessful. throw "You are not authorized to do that."; } - if (response.statusCode != 200) { + + if (response.statusCode < 200 || response.statusCode > 299) { throw "Unknown Error${kDebugMode ? ": ${response.statusCode}|${response.body}" : ""}"; } } @@ -151,4 +159,10 @@ class ApiClient { headers.addAll(authorizationHeader); return http.delete(buildFullUri(path), headers: headers); } + + Future patch(String path, {Map? headers}) { + headers ??= {}; + headers.addAll(authorizationHeader); + return http.patch(buildFullUri(path), headers: headers); + } } diff --git a/lib/clients/settings_client.dart b/lib/clients/settings_client.dart index 0f7666e..dc9cae2 100644 --- a/lib/clients/settings_client.dart +++ b/lib/clients/settings_client.dart @@ -7,19 +7,30 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; class SettingsClient { static const String _settingsKey = "settings"; static const _storage = FlutterSecureStorage(); + final List Function(Settings oldSettings, Settings newSettings)> _listeners = []; Settings _currentSettings = Settings(); + void addListener(Future Function(Settings oldSettings, Settings newSettings) listener) { + _listeners.add(listener); + } + + Future notifyListeners(Settings oldSettings, Settings newSettings) async { + for(final listener in _listeners) { + await listener.call(oldSettings, newSettings); + } + } + Settings get currentSettings => _currentSettings; Future loadSettings() async { final data = await _storage.read(key: _settingsKey); if (data == null) return; _currentSettings = Settings.fromMap(jsonDecode(data)); - } Future changeSettings(Settings newSettings) async { - _currentSettings = newSettings; await _storage.write(key: _settingsKey, value: jsonEncode(newSettings.toMap())); + await notifyListeners(_currentSettings, newSettings); + _currentSettings = newSettings; } } \ No newline at end of file diff --git a/lib/models/settings.dart b/lib/models/settings.dart index da55744..2976a11 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:contacts_plus_plus/models/friend.dart'; import 'package:contacts_plus_plus/models/sem_ver.dart'; @@ -34,26 +35,25 @@ class SettingsEntry { class Settings { final SettingsEntry notificationsDenied; - final SettingsEntry unreadCheckIntervalMinutes; + final SettingsEntry publicMachineId; final SettingsEntry lastOnlineStatus; final SettingsEntry lastDismissedVersion; Settings({ SettingsEntry? notificationsDenied, - SettingsEntry? unreadCheckIntervalMinutes, + SettingsEntry? publicMachineId, SettingsEntry? lastOnlineStatus, SettingsEntry? lastDismissedVersion }) : notificationsDenied = notificationsDenied ?? const SettingsEntry(deflt: false), - unreadCheckIntervalMinutes = unreadCheckIntervalMinutes ?? const SettingsEntry(deflt: 60), + publicMachineId = publicMachineId ?? SettingsEntry(deflt: Aux.generateMachineId(),), lastOnlineStatus = lastOnlineStatus ?? SettingsEntry(deflt: OnlineStatus.online.index), - lastDismissedVersion = lastDismissedVersion ?? SettingsEntry(deflt: SemVer.zero().toString()) - ; + lastDismissedVersion = lastDismissedVersion ?? SettingsEntry(deflt: SemVer.zero().toString()); factory Settings.fromMap(Map map) { return Settings( notificationsDenied: retrieveEntryOrNull(map["notificationsDenied"]), - unreadCheckIntervalMinutes: retrieveEntryOrNull(map["unreadCheckIntervalMinutes"]), + publicMachineId: retrieveEntryOrNull(map["publicMachineId"]), lastOnlineStatus: retrieveEntryOrNull(map["lastOnlineStatus"]), lastDismissedVersion: retrieveEntryOrNull(map["lastDismissedVersion"]) ); @@ -71,7 +71,7 @@ class Settings { Map toMap() { return { "notificationsDenied": notificationsDenied.toMap(), - "unreadCheckIntervalMinutes": unreadCheckIntervalMinutes.toMap(), + "publicMachineId": publicMachineId.toMap(), "lastOnlineStatus": lastOnlineStatus.toMap(), "lastDismissedVersion": lastDismissedVersion.toMap(), }; @@ -81,13 +81,13 @@ class Settings { Settings copyWith({ bool? notificationsDenied, - int? unreadCheckIntervalMinutes, + String? publicMachineId, int? lastOnlineStatus, String? lastDismissedVersion, }) { return Settings( notificationsDenied: this.notificationsDenied.passThrough(notificationsDenied), - unreadCheckIntervalMinutes: this.unreadCheckIntervalMinutes.passThrough(unreadCheckIntervalMinutes), + publicMachineId: this.publicMachineId.passThrough(publicMachineId), lastOnlineStatus: this.lastOnlineStatus.passThrough(lastOnlineStatus), lastDismissedVersion: this.lastDismissedVersion.passThrough(lastDismissedVersion), ); diff --git a/lib/models/user_profile.dart b/lib/models/user_profile.dart index e0b645a..f78504a 100644 --- a/lib/models/user_profile.dart +++ b/lib/models/user_profile.dart @@ -5,8 +5,8 @@ class UserProfile { factory UserProfile.empty() => UserProfile(iconUrl: ""); - factory UserProfile.fromMap(Map map) { - return UserProfile(iconUrl: map["iconUrl"] ?? ""); + factory UserProfile.fromMap(Map? map) { + return UserProfile(iconUrl: map?["iconUrl"] ?? ""); } Map toMap() { diff --git a/lib/widgets/friends/user_list_tile.dart b/lib/widgets/friends/user_list_tile.dart index f7614e3..764601c 100644 --- a/lib/widgets/friends/user_list_tile.dart +++ b/lib/widgets/friends/user_list_tile.dart @@ -67,7 +67,7 @@ class _UserListTileState extends State { _loading = false; _localAdded = !_localAdded; }); - widget.onChanged?.call(); + await widget.onChanged?.call(); } catch (e, s) { FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s)); ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/widgets/friends/user_search.dart b/lib/widgets/friends/user_search.dart index ae8c8ab..fd90f2b 100644 --- a/lib/widgets/friends/user_search.dart +++ b/lib/widgets/friends/user_search.dart @@ -70,8 +70,10 @@ class _UserSearchState extends State { itemCount: users.length, itemBuilder: (context, index) { final user = users[index]; - return UserListTile(user: user, onChanged: () { - mClient.refreshFriendsList(); + return UserListTile(user: user, onChanged: () async { + try { + await mClient.refreshFriendsList(); + } catch (_) {} }, isFriend: mClient.getAsFriend(user.id) != null,); }, );