OpenContacts/lib/clients/api_client.dart

222 lines
7.9 KiB
Dart
Raw Normal View History

import 'dart:async';
2023-04-29 13:18:46 -04:00
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;
2024-07-15 00:23:04 -04:00
import 'package:OpenContacts/models/authentication_data.dart';
2023-05-16 07:00:17 -04:00
import 'package:logging/logging.dart';
2023-04-29 13:18:46 -04:00
import 'package:uuid/uuid.dart';
import '../config.dart';
2023-04-29 13:18:46 -04:00
class ApiClient {
static const String totpKey = "TOTP";
2023-04-29 13:18:46 -04:00
static const String userIdKey = "userId";
static const String machineIdKey = "machineId";
static const String tokenKey = "token";
2023-04-29 15:26:12 -04:00
static const String passwordKey = "password";
static const String uidKey = "uid";
2023-04-29 13:18:46 -04:00
2023-06-17 10:58:32 -04:00
ApiClient({required AuthenticationData authenticationData, required this.onLogout})
: _authenticationData = authenticationData;
2023-04-29 16:21:00 -04:00
2023-04-30 07:39:09 -04:00
final AuthenticationData _authenticationData;
2023-05-16 07:00:17 -04:00
final Logger _logger = Logger("API");
2023-06-17 10:58:32 -04:00
// Saving the context here feels kinda cringe ngl
final Function() onLogout;
2023-06-17 10:58:32 -04:00
final http.Client _client = http.Client();
2023-04-29 15:26:12 -04:00
AuthenticationData get authenticationData => _authenticationData;
2023-06-17 10:58:32 -04:00
2023-04-30 07:39:09 -04:00
String get userId => _authenticationData.userId;
2023-06-17 10:58:32 -04:00
2023-04-30 07:39:09 -04:00
bool get isAuthenticated => _authenticationData.isAuthenticated;
2023-04-29 13:18:46 -04:00
2023-04-29 15:26:12 -04:00
static Future<AuthenticationData> tryLogin({
required String username,
required String password,
2023-06-17 10:58:32 -04:00
bool rememberMe = true,
bool rememberPass = true,
String? oneTimePad,
2023-04-29 15:26:12 -04:00
}) async {
2023-04-29 13:18:46 -04:00
final body = {
2023-05-15 06:13:46 -04:00
(username.contains("@") ? "email" : "username"): username.trim(),
2023-09-29 03:51:46 -04:00
"authentication": {
"\$type": "password",
"password": password,
},
2023-04-29 13:18:46 -04:00
"rememberMe": rememberMe,
"secretMachineId": const Uuid().v4(),
};
final uid = const Uuid().v4().replaceAll("-", "");
2023-04-29 13:18:46 -04:00
final response = await http.post(
2023-09-29 03:51:46 -04:00
buildFullUri("/userSessions"),
2023-06-17 10:58:32 -04:00
headers: {
"Content-Type": "application/json",
"UID": uid,
2023-06-17 10:58:32 -04:00
if (oneTimePad != null) totpKey: oneTimePad,
},
body: jsonEncode(body),
);
if (response.statusCode == 403 && response.body == totpKey) {
throw totpKey;
}
2023-04-29 13:18:46 -04:00
if (response.statusCode == 400) {
throw "Invalid Credentials";
2023-06-17 10:58:32 -04:00
}
2023-05-25 09:50:38 -04:00
checkResponseCode(response);
final data = jsonDecode(response.body);
data["entity"]["uid"] = uid;
final authData = AuthenticationData.fromMap(data);
2023-04-29 13:18:46 -04:00
if (authData.isAuthenticated) {
2023-05-25 09:50:38 -04:00
const FlutterSecureStorage storage = FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
);
2023-04-29 13:18:46 -04:00
await storage.write(key: userIdKey, value: authData.userId);
2023-09-29 03:51:46 -04:00
await storage.write(key: machineIdKey, value: authData.secretMachineIdHash);
2023-04-29 13:18:46 -04:00
await storage.write(key: tokenKey, value: authData.token);
await storage.write(key: uidKey, value: authData.uid);
2023-04-29 15:26:12 -04:00
if (rememberPass) await storage.write(key: passwordKey, value: password);
2023-04-29 13:18:46 -04:00
}
return authData;
}
static Future<AuthenticationData> tryCachedLogin() async {
2023-05-25 09:50:38 -04:00
const FlutterSecureStorage storage = FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
);
2023-04-29 13:18:46 -04:00
String? userId = await storage.read(key: userIdKey);
String? machineId = await storage.read(key: machineIdKey);
String? token = await storage.read(key: tokenKey);
2023-04-29 15:26:12 -04:00
String? password = await storage.read(key: passwordKey);
String? uid = await storage.read(key: uidKey);
2023-04-29 13:18:46 -04:00
if (userId == null || machineId == null || uid == null) {
2023-04-29 13:18:46 -04:00
return AuthenticationData.unauthenticated();
}
2023-04-29 15:26:12 -04:00
if (token != null) {
2023-09-29 03:51:46 -04:00
final response = await http.patch(buildFullUri("/userSessions"), headers: {
"Authorization": "res $userId:$token",
"UID": uid,
2023-09-29 03:51:46 -04:00
});
if (response.statusCode < 300) {
return AuthenticationData(
userId: userId,
token: token,
secretMachineIdHash: machineId,
isAuthenticated: true,
uid: uid,
);
2023-04-29 15:26:12 -04:00
}
}
if (password != null) {
try {
userId = userId.startsWith("U-") ? userId.replaceRange(0, 2, "") : userId;
final loginResult = await tryLogin(username: userId, password: password, rememberPass: true);
if (loginResult.isAuthenticated) return loginResult;
} catch (_) {
// We don't need to notify the user if the cached login fails behind the scenes, so just ignore any exceptions.
}
2023-04-29 13:18:46 -04:00
}
return AuthenticationData.unauthenticated();
}
Future<void> logout() async {
2023-05-25 09:50:38 -04:00
const FlutterSecureStorage storage = FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
);
await storage.delete(key: userIdKey);
await storage.delete(key: machineIdKey);
await storage.delete(key: tokenKey);
await storage.delete(key: passwordKey);
onLogout();
}
2023-05-15 06:13:46 -04:00
Future<void> extendSession() async {
final response = await patch("/userSessions");
if (response.statusCode != 204) {
throw "Failed to extend session.";
}
}
2023-05-25 09:50:38 -04:00
void checkResponse(http.Response response) {
2023-05-02 04:22:04 -04:00
if (response.statusCode == 403) {
2023-05-25 09:50:38 -04:00
tryCachedLogin().then((value) {
if (!value.isAuthenticated) {
onLogout();
2023-05-25 09:50:38 -04:00
}
});
2023-04-29 15:26:12 -04:00
}
2023-05-25 09:50:38 -04:00
checkResponseCode(response);
}
static void checkResponseCode(http.Response response) {
if (response.statusCode < 300) return;
2023-06-17 10:58:32 -04:00
final error =
"${response.request?.method ?? "Unknown Method"}|${response.request?.url ?? "Unknown URL"}: ${switch (response.statusCode) {
429 => "You are being rate limited.",
2023-05-25 09:50:38 -04:00
403 => "You are not authorized to do that.",
404 => "Resource not found.",
500 => "Internal server error.",
_ => "Unknown Error."
2023-06-17 10:58:32 -04:00
}} (${response.statusCode}${kDebugMode && response.body.isNotEmpty ? "|${response.body}" : ""})";
2023-05-25 09:50:38 -04:00
2023-09-29 03:51:46 -04:00
FlutterError.reportError(FlutterErrorDetails(
exception: error,
stack: StackTrace.current,
));
2023-05-25 09:50:38 -04:00
throw error;
2023-04-29 15:26:12 -04:00
}
2023-04-29 13:18:46 -04:00
Map<String, String> get authorizationHeader => _authenticationData.authorizationHeader;
2023-04-29 13:18:46 -04:00
2023-09-29 03:51:46 -04:00
static Uri buildFullUri(String path) => Uri.parse("${Config.apiBaseUrl}$path");
2023-04-29 15:26:12 -04:00
2023-05-16 07:00:17 -04:00
Future<http.Response> get(String path, {Map<String, String>? headers}) async {
2023-04-29 13:18:46 -04:00
headers ??= {};
headers.addAll(authorizationHeader);
2023-06-17 10:58:32 -04:00
final response = await _client.get(buildFullUri(path), headers: headers);
2023-05-16 10:40:43 -04:00
_logger.info("GET $path => ${response.statusCode}${response.statusCode >= 300 ? ": ${response.body}" : ""}");
2023-05-16 07:00:17 -04:00
return response;
2023-04-29 13:18:46 -04:00
}
2023-05-16 07:00:17 -04:00
Future<http.Response> post(String path, {Object? body, Map<String, String>? headers}) async {
2023-04-29 13:18:46 -04:00
headers ??= {};
headers["Content-Type"] = "application/json";
headers.addAll(authorizationHeader);
2023-06-17 10:58:32 -04:00
final response = await _client.post(buildFullUri(path), headers: headers, body: body);
2023-05-16 10:40:43 -04:00
_logger.info("PST $path => ${response.statusCode}${response.statusCode >= 300 ? ": ${response.body}" : ""}");
2023-05-16 07:00:17 -04:00
return response;
2023-04-29 13:18:46 -04:00
}
2023-05-16 07:00:17 -04:00
Future<http.Response> put(String path, {Object? body, Map<String, String>? headers}) async {
2023-04-29 13:18:46 -04:00
headers ??= {};
headers["Content-Type"] = "application/json";
2023-04-29 13:18:46 -04:00
headers.addAll(authorizationHeader);
2023-06-17 10:58:32 -04:00
final response = await _client.put(buildFullUri(path), headers: headers, body: body);
2023-05-16 10:40:43 -04:00
_logger.info("PUT $path => ${response.statusCode}${response.statusCode >= 300 ? ": ${response.body}" : ""}");
2023-05-16 07:00:17 -04:00
return response;
2023-04-29 13:18:46 -04:00
}
2023-05-16 07:00:17 -04:00
Future<http.Response> delete(String path, {Map<String, String>? headers}) async {
2023-04-29 13:18:46 -04:00
headers ??= {};
headers.addAll(authorizationHeader);
2023-06-17 10:58:32 -04:00
final response = await _client.delete(buildFullUri(path), headers: headers);
2023-05-16 10:40:43 -04:00
_logger.info("DEL $path => ${response.statusCode}${response.statusCode >= 300 ? ": ${response.body}" : ""}");
2023-05-16 07:00:17 -04:00
return response;
2023-04-29 13:18:46 -04:00
}
2023-05-15 06:13:46 -04:00
2023-05-16 07:00:17 -04:00
Future<http.Response> patch(String path, {Object? body, Map<String, String>? headers}) async {
2023-05-15 06:13:46 -04:00
headers ??= {};
headers["Content-Type"] = "application/json";
headers.addAll(authorizationHeader);
2023-06-17 10:58:32 -04:00
final response = await _client.patch(buildFullUri(path), headers: headers, body: body);
2023-05-16 10:40:43 -04:00
_logger.info("PAT $path => ${response.statusCode}${response.statusCode >= 300 ? ": ${response.body}" : ""}");
2023-05-16 07:00:17 -04:00
return response;
2023-05-15 06:13:46 -04:00
}
2023-04-29 16:21:00 -04:00
}