2023-05-01 11:34:34 -04:00
|
|
|
import 'dart:async';
|
2023-04-29 13:18:46 -04:00
|
|
|
import 'dart:convert';
|
|
|
|
import 'package:flutter/foundation.dart';
|
2023-05-01 11:34:34 -04:00
|
|
|
import 'package:flutter/material.dart';
|
2023-05-03 11:51:18 -04:00
|
|
|
import 'package:flutter_phoenix/flutter_phoenix.dart';
|
2023-04-29 13:18:46 -04:00
|
|
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
|
|
import 'package:http/http.dart' as http;
|
2023-05-01 13:13:40 -04:00
|
|
|
import 'package:contacts_plus_plus/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';
|
|
|
|
|
2023-05-03 15:55:34 -04:00
|
|
|
import '../config.dart';
|
2023-04-29 13:18:46 -04:00
|
|
|
|
|
|
|
class ApiClient {
|
2023-05-07 11:25:00 -04:00
|
|
|
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";
|
2023-04-29 13:18:46 -04:00
|
|
|
|
2023-05-16 07:00:17 -04:00
|
|
|
ApiClient({required AuthenticationData authenticationData}) : _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-04-29 15:26:12 -04:00
|
|
|
|
2023-05-04 07:13:24 -04:00
|
|
|
AuthenticationData get authenticationData => _authenticationData;
|
2023-04-30 07:39:09 -04:00
|
|
|
String get userId => _authenticationData.userId;
|
|
|
|
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,
|
|
|
|
bool rememberMe=true,
|
2023-05-07 11:25:00 -04:00
|
|
|
bool rememberPass=false,
|
|
|
|
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-04-29 13:18:46 -04:00
|
|
|
"password": password,
|
|
|
|
"rememberMe": rememberMe,
|
|
|
|
"secretMachineId": const Uuid().v4(),
|
|
|
|
};
|
|
|
|
final response = await http.post(
|
2023-04-29 15:26:12 -04:00
|
|
|
buildFullUri("/UserSessions"),
|
2023-05-07 11:25:00 -04:00
|
|
|
headers: {
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
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-04-29 15:26:12 -04:00
|
|
|
}
|
|
|
|
checkResponse(response);
|
2023-04-29 13:18:46 -04:00
|
|
|
|
2023-04-29 15:26:12 -04:00
|
|
|
final authData = AuthenticationData.fromMap(jsonDecode(response.body));
|
2023-04-29 13:18:46 -04:00
|
|
|
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: tokenKey, value: authData.token);
|
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 {
|
|
|
|
const FlutterSecureStorage storage = FlutterSecureStorage();
|
|
|
|
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);
|
2023-04-29 13:18:46 -04:00
|
|
|
|
2023-04-29 15:26:12 -04:00
|
|
|
if (userId == null || machineId == null) {
|
2023-04-29 13:18:46 -04:00
|
|
|
return AuthenticationData.unauthenticated();
|
|
|
|
}
|
|
|
|
|
2023-04-29 15:26:12 -04:00
|
|
|
if (token != null) {
|
|
|
|
final response = await http.get(buildFullUri("/users/$userId"), headers: {
|
|
|
|
"Authorization": "neos $userId:$token"
|
|
|
|
});
|
|
|
|
if (response.statusCode == 200) {
|
|
|
|
return AuthenticationData(userId: userId, token: token, secretMachineId: machineId, isAuthenticated: true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
|
|
|
}
|
2023-05-03 11:51:18 -04:00
|
|
|
|
|
|
|
Future<void> logout(BuildContext context) async {
|
|
|
|
const FlutterSecureStorage storage = FlutterSecureStorage();
|
|
|
|
await storage.delete(key: userIdKey);
|
|
|
|
await storage.delete(key: machineIdKey);
|
|
|
|
await storage.delete(key: tokenKey);
|
|
|
|
await storage.delete(key: passwordKey);
|
|
|
|
if (context.mounted) {
|
|
|
|
Phoenix.rebirth(context);
|
|
|
|
}
|
|
|
|
}
|
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-04-29 15:26:12 -04:00
|
|
|
static void checkResponse(http.Response response) {
|
2023-05-02 04:22:04 -04:00
|
|
|
if (response.statusCode == 429) {
|
|
|
|
throw "Sorry, you are being rate limited";
|
|
|
|
}
|
|
|
|
if (response.statusCode == 403) {
|
|
|
|
tryCachedLogin();
|
|
|
|
// TODO: Show the login screen again if cached login was unsuccessful.
|
2023-05-16 09:57:44 -04:00
|
|
|
throw "You are not authorized to do that";
|
|
|
|
}
|
|
|
|
if (response.statusCode == 500) {
|
|
|
|
throw "Internal server error";
|
2023-05-02 04:22:04 -04:00
|
|
|
}
|
2023-04-29 15:26:12 -04:00
|
|
|
if (response.statusCode != 200) {
|
2023-05-16 09:57:44 -04:00
|
|
|
throw "Unknown Error: ${response.statusCode}${kDebugMode ? "|${response.body}" : ""}";
|
2023-04-29 15:26:12 -04:00
|
|
|
}
|
|
|
|
}
|
2023-04-29 13:18:46 -04:00
|
|
|
|
2023-05-01 11:34:34 -04:00
|
|
|
Map<String, String> get authorizationHeader => _authenticationData.authorizationHeader;
|
2023-04-29 13:18:46 -04:00
|
|
|
|
2023-04-29 15:26:12 -04:00
|
|
|
static Uri buildFullUri(String path) => Uri.parse("${Config.apiBaseUrl}/api$path");
|
|
|
|
|
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-05-16 07:00:17 -04:00
|
|
|
final response = await http.get(buildFullUri(path), headers: headers);
|
|
|
|
_logger.info("GET $path => ${response.statusCode}");
|
|
|
|
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-05-16 07:00:17 -04:00
|
|
|
final response = await http.post(buildFullUri(path), headers: headers, body: body);
|
|
|
|
_logger.info("PST $path => ${response.statusCode}");
|
|
|
|
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 ??= {};
|
2023-05-04 14:57:16 -04:00
|
|
|
headers["Content-Type"] = "application/json";
|
2023-04-29 13:18:46 -04:00
|
|
|
headers.addAll(authorizationHeader);
|
2023-05-16 07:00:17 -04:00
|
|
|
final response = await http.put(buildFullUri(path), headers: headers, body: body);
|
|
|
|
_logger.info("PUT $path => ${response.statusCode}");
|
|
|
|
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-05-16 07:00:17 -04:00
|
|
|
final response = await http.delete(buildFullUri(path), headers: headers);
|
|
|
|
_logger.info("DEL $path => ${response.statusCode}");
|
|
|
|
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-05-16 07:00:17 -04:00
|
|
|
final response = await http.patch(buildFullUri(path), headers: headers, body: body);
|
|
|
|
_logger.info("PAT $path => ${response.statusCode}");
|
|
|
|
return response;
|
2023-05-15 06:13:46 -04:00
|
|
|
}
|
2023-04-29 16:21:00 -04:00
|
|
|
}
|