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);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue