Add support for email login and improve general error handling
This commit is contained in:
parent
56ae580329
commit
32d8333c7c
8 changed files with 63 additions and 59 deletions
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,);
|
||||
},
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue