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);
}
static Future<void> notifyOnlineInstance(ApiClient client) async {
final response = await client.post("/stats/instanceOnline/${client.authenticationData.secretMachineId.hashCode}");
static Future<void> notifyOnlineInstance(ApiClient client, {required String machineId}) async {
final response = await client.post("/stats/instanceOnline/$machineId");
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: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();
}

View file

@ -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<AuthenticationData> 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<void> extendSession() async {
final response = await patch("/userSessions");
if (response.statusCode != 204) {
throw "Failed to extend session.";
}
}
Future<void> 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<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 {
static const String _settingsKey = "settings";
static const _storage = FlutterSecureStorage();
final List<Future<void> Function(Settings oldSettings, Settings newSettings)> _listeners = [];
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;
Future<void> loadSettings() async {
final data = await _storage.read(key: _settingsKey);
if (data == null) return;
_currentSettings = Settings.fromMap(jsonDecode(data));
}
Future<void> changeSettings(Settings newSettings) async {
_currentSettings = newSettings;
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 '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<T> {
class Settings {
final SettingsEntry<bool> notificationsDenied;
final SettingsEntry<int> unreadCheckIntervalMinutes;
final SettingsEntry<String> publicMachineId;
final SettingsEntry<int> lastOnlineStatus;
final SettingsEntry<String> lastDismissedVersion;
Settings({
SettingsEntry<bool>? notificationsDenied,
SettingsEntry<int>? unreadCheckIntervalMinutes,
SettingsEntry<String>? publicMachineId,
SettingsEntry<int>? lastOnlineStatus,
SettingsEntry<String>? lastDismissedVersion
})
: 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),
lastDismissedVersion = lastDismissedVersion ?? SettingsEntry<String>(deflt: SemVer.zero().toString())
;
lastDismissedVersion = lastDismissedVersion ?? SettingsEntry<String>(deflt: SemVer.zero().toString());
factory Settings.fromMap(Map map) {
return Settings(
notificationsDenied: retrieveEntryOrNull<bool>(map["notificationsDenied"]),
unreadCheckIntervalMinutes: retrieveEntryOrNull<int>(map["unreadCheckIntervalMinutes"]),
publicMachineId: retrieveEntryOrNull<String>(map["publicMachineId"]),
lastOnlineStatus: retrieveEntryOrNull<int>(map["lastOnlineStatus"]),
lastDismissedVersion: retrieveEntryOrNull<String>(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),
);

View file

@ -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() {

View file

@ -67,7 +67,7 @@ class _UserListTileState extends State<UserListTile> {
_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(

View file

@ -70,8 +70,10 @@ class _UserSearchState extends State<UserSearch> {
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,);
},
);