Add support for email login and improve general error handling

This commit is contained in:
Nutcake 2023-05-09 10:49:20 +02:00
parent 56ae580329
commit 32d8333c7c
8 changed files with 63 additions and 59 deletions

View file

@ -29,8 +29,8 @@ class UserApi {
return UserStatus.fromMap(data); return UserStatus.fromMap(data);
} }
static Future<void> notifyOnlineInstance(ApiClient client) async { static Future<void> notifyOnlineInstance(ApiClient client, {required String machineId}) async {
final response = await client.post("/stats/instanceOnline/${client.authenticationData.secretMachineId.hashCode}"); final response = await client.post("/stats/instanceOnline/$machineId");
ApiClient.checkResponse(response); ApiClient.checkResponse(response);
} }

View file

@ -1,6 +1,9 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:contacts_plus_plus/config.dart'; import 'package:contacts_plus_plus/config.dart';
import 'package:path/path.dart' as p;
import 'package:html/parser.dart' as htmlparser; import 'package:html/parser.dart' as htmlparser;
import 'package:uuid/uuid.dart';
enum NeosDBEndpoint enum NeosDBEndpoint
{ {
@ -10,39 +13,6 @@ enum NeosDBEndpoint
videoCDN, 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 { class Aux {
static String neosDbToHttp(String? neosdb) { static String neosDbToHttp(String? neosdb) {
if (neosdb == null || neosdb.isEmpty) return ""; if (neosdb == null || neosdb.isEmpty) return "";
@ -55,6 +25,13 @@ class Aux {
} }
return fullUri; return fullUri;
} }
static String toURLBase64(Uint8List data) => base64.encode(data)
.replaceAll("+", "-")
.replaceAll("/", "_")
.replaceAll("=", "");
static String generateMachineId() => Aux.toURLBase64((const Uuid().v1obj().toBytes())).toLowerCase();
} }

View file

@ -13,11 +13,11 @@ import '../config.dart';
class ApiClient { class ApiClient {
static const String totpKey = "TOTP"; static const String totpKey = "TOTP";
static const String userIdKey = "userId"; static const String userIdKey = "userId";
static const String machineIdKey = "machineId"; static const String secretMachineIdKey = "machineId";
static const String tokenKey = "token"; static const String tokenKey = "token";
static const String passwordKey = "password"; static const String passwordKey = "password";
ApiClient({required AuthenticationData authenticationData}) : _authenticationData = authenticationData; const ApiClient({required AuthenticationData authenticationData}) : _authenticationData = authenticationData;
final AuthenticationData _authenticationData; final AuthenticationData _authenticationData;
@ -33,7 +33,7 @@ class ApiClient {
String? oneTimePad, String? oneTimePad,
}) async { }) async {
final body = { final body = {
"username": username, (username.contains("@") ? "email" : "username"): username.trim(),
"password": password, "password": password,
"rememberMe": rememberMe, "rememberMe": rememberMe,
"secretMachineId": const Uuid().v4(), "secretMachineId": const Uuid().v4(),
@ -58,7 +58,7 @@ class ApiClient {
if (authData.isAuthenticated) { if (authData.isAuthenticated) {
const FlutterSecureStorage storage = FlutterSecureStorage(); const FlutterSecureStorage storage = FlutterSecureStorage();
await storage.write(key: userIdKey, value: authData.userId); 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); await storage.write(key: tokenKey, value: authData.token);
if (rememberPass) await storage.write(key: passwordKey, value: password); if (rememberPass) await storage.write(key: passwordKey, value: password);
} }
@ -68,7 +68,7 @@ class ApiClient {
static Future<AuthenticationData> tryCachedLogin() async { static Future<AuthenticationData> tryCachedLogin() async {
const FlutterSecureStorage storage = FlutterSecureStorage(); const FlutterSecureStorage storage = FlutterSecureStorage();
String? userId = await storage.read(key: userIdKey); 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? token = await storage.read(key: tokenKey);
String? password = await storage.read(key: passwordKey); String? password = await storage.read(key: passwordKey);
@ -97,10 +97,17 @@ class ApiClient {
return AuthenticationData.unauthenticated(); return AuthenticationData.unauthenticated();
} }
Future<void> extendSession() async {
final response = await patch("/userSessions");
if (response.statusCode != 204) {
throw "Failed to extend session.";
}
}
Future<void> logout(BuildContext context) async { Future<void> logout(BuildContext context) async {
const FlutterSecureStorage storage = FlutterSecureStorage(); const FlutterSecureStorage storage = FlutterSecureStorage();
await storage.delete(key: userIdKey); await storage.delete(key: userIdKey);
await storage.delete(key: machineIdKey); await storage.delete(key: secretMachineIdKey);
await storage.delete(key: tokenKey); await storage.delete(key: tokenKey);
await storage.delete(key: passwordKey); await storage.delete(key: passwordKey);
if (context.mounted) { if (context.mounted) {
@ -117,7 +124,8 @@ class ApiClient {
// TODO: Show the login screen again if cached login was unsuccessful. // TODO: Show the login screen again if cached login was unsuccessful.
throw "You are not authorized to do that."; 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}" : ""}"; throw "Unknown Error${kDebugMode ? ": ${response.statusCode}|${response.body}" : ""}";
} }
} }
@ -151,4 +159,10 @@ class ApiClient {
headers.addAll(authorizationHeader); headers.addAll(authorizationHeader);
return http.delete(buildFullUri(path), headers: headers); return http.delete(buildFullUri(path), headers: headers);
} }
Future<http.Response> patch(String path, {Map<String, String>? headers}) {
headers ??= {};
headers.addAll(authorizationHeader);
return http.patch(buildFullUri(path), headers: headers);
}
} }

View file

@ -7,19 +7,30 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SettingsClient { class SettingsClient {
static const String _settingsKey = "settings"; static const String _settingsKey = "settings";
static const _storage = FlutterSecureStorage(); static const _storage = FlutterSecureStorage();
final List<Future<void> Function(Settings oldSettings, Settings newSettings)> _listeners = [];
Settings _currentSettings = Settings(); Settings _currentSettings = Settings();
void addListener(Future<void> Function(Settings oldSettings, Settings newSettings) listener) {
_listeners.add(listener);
}
Future<void> notifyListeners(Settings oldSettings, Settings newSettings) async {
for(final listener in _listeners) {
await listener.call(oldSettings, newSettings);
}
}
Settings get currentSettings => _currentSettings; Settings get currentSettings => _currentSettings;
Future<void> loadSettings() async { Future<void> loadSettings() async {
final data = await _storage.read(key: _settingsKey); final data = await _storage.read(key: _settingsKey);
if (data == null) return; if (data == null) return;
_currentSettings = Settings.fromMap(jsonDecode(data)); _currentSettings = Settings.fromMap(jsonDecode(data));
} }
Future<void> changeSettings(Settings newSettings) async { Future<void> changeSettings(Settings newSettings) async {
_currentSettings = newSettings;
await _storage.write(key: _settingsKey, value: jsonEncode(newSettings.toMap())); await _storage.write(key: _settingsKey, value: jsonEncode(newSettings.toMap()));
await notifyListeners(_currentSettings, newSettings);
_currentSettings = newSettings;
} }
} }

View file

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:contacts_plus_plus/auxiliary.dart';
import 'package:contacts_plus_plus/models/friend.dart'; import 'package:contacts_plus_plus/models/friend.dart';
import 'package:contacts_plus_plus/models/sem_ver.dart'; import 'package:contacts_plus_plus/models/sem_ver.dart';
@ -34,26 +35,25 @@ class SettingsEntry<T> {
class Settings { class Settings {
final SettingsEntry<bool> notificationsDenied; final SettingsEntry<bool> notificationsDenied;
final SettingsEntry<int> unreadCheckIntervalMinutes; final SettingsEntry<String> publicMachineId;
final SettingsEntry<int> lastOnlineStatus; final SettingsEntry<int> lastOnlineStatus;
final SettingsEntry<String> lastDismissedVersion; final SettingsEntry<String> lastDismissedVersion;
Settings({ Settings({
SettingsEntry<bool>? notificationsDenied, SettingsEntry<bool>? notificationsDenied,
SettingsEntry<int>? unreadCheckIntervalMinutes, SettingsEntry<String>? publicMachineId,
SettingsEntry<int>? lastOnlineStatus, SettingsEntry<int>? lastOnlineStatus,
SettingsEntry<String>? lastDismissedVersion SettingsEntry<String>? lastDismissedVersion
}) })
: notificationsDenied = notificationsDenied ?? const SettingsEntry<bool>(deflt: false), : notificationsDenied = notificationsDenied ?? const SettingsEntry<bool>(deflt: false),
unreadCheckIntervalMinutes = unreadCheckIntervalMinutes ?? const SettingsEntry<int>(deflt: 60), publicMachineId = publicMachineId ?? SettingsEntry<String>(deflt: Aux.generateMachineId(),),
lastOnlineStatus = lastOnlineStatus ?? SettingsEntry<int>(deflt: OnlineStatus.online.index), lastOnlineStatus = lastOnlineStatus ?? SettingsEntry<int>(deflt: OnlineStatus.online.index),
lastDismissedVersion = lastDismissedVersion ?? SettingsEntry<String>(deflt: SemVer.zero().toString()) lastDismissedVersion = lastDismissedVersion ?? SettingsEntry<String>(deflt: SemVer.zero().toString());
;
factory Settings.fromMap(Map map) { factory Settings.fromMap(Map map) {
return Settings( return Settings(
notificationsDenied: retrieveEntryOrNull<bool>(map["notificationsDenied"]), notificationsDenied: retrieveEntryOrNull<bool>(map["notificationsDenied"]),
unreadCheckIntervalMinutes: retrieveEntryOrNull<int>(map["unreadCheckIntervalMinutes"]), publicMachineId: retrieveEntryOrNull<String>(map["publicMachineId"]),
lastOnlineStatus: retrieveEntryOrNull<int>(map["lastOnlineStatus"]), lastOnlineStatus: retrieveEntryOrNull<int>(map["lastOnlineStatus"]),
lastDismissedVersion: retrieveEntryOrNull<String>(map["lastDismissedVersion"]) lastDismissedVersion: retrieveEntryOrNull<String>(map["lastDismissedVersion"])
); );
@ -71,7 +71,7 @@ class Settings {
Map toMap() { Map toMap() {
return { return {
"notificationsDenied": notificationsDenied.toMap(), "notificationsDenied": notificationsDenied.toMap(),
"unreadCheckIntervalMinutes": unreadCheckIntervalMinutes.toMap(), "publicMachineId": publicMachineId.toMap(),
"lastOnlineStatus": lastOnlineStatus.toMap(), "lastOnlineStatus": lastOnlineStatus.toMap(),
"lastDismissedVersion": lastDismissedVersion.toMap(), "lastDismissedVersion": lastDismissedVersion.toMap(),
}; };
@ -81,13 +81,13 @@ class Settings {
Settings copyWith({ Settings copyWith({
bool? notificationsDenied, bool? notificationsDenied,
int? unreadCheckIntervalMinutes, String? publicMachineId,
int? lastOnlineStatus, int? lastOnlineStatus,
String? lastDismissedVersion, String? lastDismissedVersion,
}) { }) {
return Settings( return Settings(
notificationsDenied: this.notificationsDenied.passThrough(notificationsDenied), notificationsDenied: this.notificationsDenied.passThrough(notificationsDenied),
unreadCheckIntervalMinutes: this.unreadCheckIntervalMinutes.passThrough(unreadCheckIntervalMinutes), publicMachineId: this.publicMachineId.passThrough(publicMachineId),
lastOnlineStatus: this.lastOnlineStatus.passThrough(lastOnlineStatus), lastOnlineStatus: this.lastOnlineStatus.passThrough(lastOnlineStatus),
lastDismissedVersion: this.lastDismissedVersion.passThrough(lastDismissedVersion), lastDismissedVersion: this.lastDismissedVersion.passThrough(lastDismissedVersion),
); );

View file

@ -5,8 +5,8 @@ class UserProfile {
factory UserProfile.empty() => UserProfile(iconUrl: ""); factory UserProfile.empty() => UserProfile(iconUrl: "");
factory UserProfile.fromMap(Map map) { factory UserProfile.fromMap(Map? map) {
return UserProfile(iconUrl: map["iconUrl"] ?? ""); return UserProfile(iconUrl: map?["iconUrl"] ?? "");
} }
Map toMap() { Map toMap() {

View file

@ -67,7 +67,7 @@ class _UserListTileState extends State<UserListTile> {
_loading = false; _loading = false;
_localAdded = !_localAdded; _localAdded = !_localAdded;
}); });
widget.onChanged?.call(); await widget.onChanged?.call();
} catch (e, s) { } catch (e, s) {
FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s)); FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s));
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(

View file

@ -70,8 +70,10 @@ class _UserSearchState extends State<UserSearch> {
itemCount: users.length, itemCount: users.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final user = users[index]; final user = users[index];
return UserListTile(user: user, onChanged: () { return UserListTile(user: user, onChanged: () async {
mClient.refreshFriendsList(); try {
await mClient.refreshFriendsList();
} catch (_) {}
}, isFriend: mClient.getAsFriend(user.id) != null,); }, isFriend: mClient.getAsFriend(user.id) != null,);
}, },
); );