Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
303 changes: 155 additions & 148 deletions lib/apis/record_api.dart

Large diffs are not rendered by default.

106 changes: 42 additions & 64 deletions lib/clients/api_client.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
import 'package:logging/logging.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:recon/auxiliary.dart';
import 'package:recon/models/authentication_data.dart';
import 'package:uuid/uuid.dart';
Expand All @@ -19,13 +22,14 @@ class ApiClient {
static const String passwordKey = "password";
static const String uidKey = "uid";

ApiClient({required AuthenticationData authenticationData}) : _authenticationData = authenticationData;
ApiClient({required this._authenticationData, required this._pkgInfo});

final AuthenticationData _authenticationData;
final Logger _logger = Logger("API");

final _logoutNotifier = EventNotifier();
final http.Client _client = http.Client();
late final _client = IOClient(HttpClient()..userAgent = "${_pkgInfo.appName}/${_pkgInfo.version}");
final PackageInfo _pkgInfo;

AuthenticationData get authenticationData => _authenticationData;

Expand All @@ -35,32 +39,15 @@ class ApiClient {

void addLogoutListener(VoidCallback listener) => _logoutNotifier.addListener(listener);

static Future<AuthenticationData> tryLogin({
required String username,
required String password,
bool rememberMe = true,
bool rememberPass = true,
String? oneTimePad,
}) async {
static Future<AuthenticationData> tryLogin({required String username, required String password, bool rememberMe = true, bool rememberPass = true, String? oneTimePad}) async {
final body = {
(username.contains("@") ? "email" : "username"): username.trim(),
"authentication": {
r"$type": "password",
"password": password,
},
"authentication": {r"$type": "password", "password": password},
"rememberMe": rememberMe,
"secretMachineId": const Uuid().v4(),
};
final uid = const Uuid().v4().replaceAll("-", "");
final response = await http.post(
buildFullUri("/userSessions"),
headers: {
"Content-Type": "application/json",
"UID": uid,
if (oneTimePad != null) totpKey: oneTimePad,
},
body: jsonEncode(body),
);
final response = await http.post(buildFullUri("/userSessions"), headers: {"Content-Type": "application/json", "UID": uid, totpKey: ?oneTimePad}, body: jsonEncode(body));
if (response.statusCode == 403 && response.body == totpKey) {
throw totpKey;
}
Expand All @@ -72,9 +59,7 @@ class ApiClient {
(data["entity"] as Map)["uid"] = uid;
final authData = AuthenticationData.fromMap(data);
if (authData.isAuthenticated) {
const storage = FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
);
const storage = FlutterSecureStorage(aOptions: AndroidOptions.defaultOptions);
await storage.write(key: userIdKey, value: authData.userId);
await storage.write(key: machineIdKey, value: authData.secretMachineIdHash);
await storage.write(key: tokenKey, value: authData.token);
Expand All @@ -85,9 +70,7 @@ class ApiClient {
}

static Future<AuthenticationData> tryCachedLogin() async {
const storage = FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
);
const storage = FlutterSecureStorage(aOptions: AndroidOptions.defaultOptions);
var userId = await storage.read(key: userIdKey);
final machineId = await storage.read(key: machineIdKey);
final token = await storage.read(key: tokenKey);
Expand All @@ -99,21 +82,9 @@ class ApiClient {
}

if (token != null) {
final response = await http.patch(
buildFullUri("/userSessions"),
headers: {
"Authorization": "res $userId:$token",
"UID": uid,
},
);
final response = await http.patch(buildFullUri("/userSessions"), headers: {"Authorization": "res $userId:$token", "UID": uid});
if (response.statusCode < 300) {
return AuthenticationData(
userId: userId,
token: token,
secretMachineIdHash: machineId,
isAuthenticated: true,
uid: uid,
);
return AuthenticationData(userId: userId, token: token, secretMachineIdHash: machineId, isAuthenticated: true, uid: uid);
}
}

Expand All @@ -131,9 +102,7 @@ class ApiClient {

Future<void> logout() async {
//TODO: Fix messaging/hub clients not being disposed on logout
const storage = FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
);
const storage = FlutterSecureStorage(aOptions: AndroidOptions.defaultOptions);
await storage.delete(key: userIdKey);
await storage.delete(key: machineIdKey);
await storage.delete(key: tokenKey);
Expand Down Expand Up @@ -162,31 +131,40 @@ class ApiClient {
static void checkResponseCode(http.Response response) {
if (response.statusCode < 300) return;

final error = "${response.request?.method ?? "Unknown Method"}|${response.request?.url ?? "Unknown URL"}: ${switch (response.statusCode) {
429 => "You are being rate limited.",
403 => "You are not authorized to do that.",
404 => "Resource not found.",
500 => "Internal server error.",
_ => "Unknown Error."
}} (${response.statusCode}${kDebugMode && response.body.isNotEmpty ? "|${response.body}" : ""})";

FlutterError.reportError(
FlutterErrorDetails(
exception: error,
stack: StackTrace.current,
),
);
final error =
"${response.request?.method ?? "Unknown Method"}|${response.request?.url ?? "Unknown URL"}: ${switch (response.statusCode) {
429 => "You are being rate limited.",
403 => "You are not authorized to do that.",
404 => "Resource not found.",
500 => "Internal server error.",
_ => "Unknown Error.",
}} (${response.statusCode}${kDebugMode && response.body.isNotEmpty ? "|${response.body}" : ""})";

FlutterError.reportError(FlutterErrorDetails(exception: error, stack: StackTrace.current));
throw error;
}

Map<String, String> get authorizationHeader => _authenticationData.authorizationHeader;

static Uri buildFullUri(String path) => Uri.parse("${Config.apiBaseUrl}$path");

Future<http.Response> _requestWrapper(Future<http.Response> Function() requestFunc) async {
int responseCode;
var attempts = 0;
http.Response response;
do {
await Future.delayed(Duration(seconds: attempts));
response = await requestFunc();
attempts++;
responseCode = response.statusCode;
} while (responseCode == 429 && attempts < 5);
return response;
}

Future<http.Response> get(String path, {Map<String, String>? headers}) async {
headers ??= {};
headers.addAll(authorizationHeader);
final response = await _client.get(buildFullUri(path), headers: headers);
final response = await _requestWrapper(() async => _client.get(buildFullUri(path), headers: headers));
_logger.info("GET $path => ${response.statusCode}${response.statusCode >= 300 ? ": ${response.body}" : ""}");
return response;
}
Expand All @@ -195,7 +173,7 @@ class ApiClient {
headers ??= {};
headers["Content-Type"] = "application/json";
headers.addAll(authorizationHeader);
final response = await _client.post(buildFullUri(path), headers: headers, body: body);
final response = await _requestWrapper(() async => _client.post(buildFullUri(path), headers: headers, body: body));
_logger.info("PST $path => ${response.statusCode}${response.statusCode >= 300 ? ": ${response.body}" : ""}");
return response;
}
Expand All @@ -204,15 +182,15 @@ class ApiClient {
headers ??= {};
headers["Content-Type"] = "application/json";
headers.addAll(authorizationHeader);
final response = await _client.put(buildFullUri(path), headers: headers, body: body);
final response = await _requestWrapper(() async => _client.put(buildFullUri(path), headers: headers, body: body));
_logger.info("PUT $path => ${response.statusCode}${response.statusCode >= 300 ? ": ${response.body}" : ""}");
return response;
}

Future<http.Response> delete(String path, {Map<String, String>? headers}) async {
headers ??= {};
headers.addAll(authorizationHeader);
final response = await _client.delete(buildFullUri(path), headers: headers);
final response = await _requestWrapper(() async => _client.delete(buildFullUri(path), headers: headers));
_logger.info("DEL $path => ${response.statusCode}${response.statusCode >= 300 ? ": ${response.body}" : ""}");
return response;
}
Expand All @@ -221,7 +199,7 @@ class ApiClient {
headers ??= {};
headers["Content-Type"] = "application/json";
headers.addAll(authorizationHeader);
final response = await _client.patch(buildFullUri(path), headers: headers, body: body);
final response = await _requestWrapper(() async => _client.patch(buildFullUri(path), headers: headers, body: body));
_logger.info("PAT $path => ${response.statusCode}${response.statusCode >= 300 ? ": ${response.body}" : ""}");
return response;
}
Expand Down
94 changes: 36 additions & 58 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,42 +36,37 @@ void main() async {
log("Failed to initialize JustAudioMediaKit, audio features will be unavailable. Error: $e");
}
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
systemStatusBarContrastEnforced: true,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
),
const SystemUiOverlayStyle(systemStatusBarContrastEnforced: true, systemNavigationBarColor: Colors.transparent, systemNavigationBarDividerColor: Colors.transparent),
);

await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge, overlays: [SystemUiOverlay.top]);

await Hive.initFlutter();

final dateFormat = DateFormat.Hms();
Logger.root.onRecord.listen(
(event) => log("${dateFormat.format(event.time)}: ${event.message}", name: event.loggerName, time: event.time),
);
Logger.root.onRecord.listen((event) => log("${dateFormat.format(event.time)}: ${event.message}", name: event.loggerName, time: event.time));
Logger.root.level = Level.WARNING;
final settingsClient = SettingsClient();
await settingsClient.loadSettings();
final newSettings = settingsClient.currentSettings.copyWith(machineId: settingsClient.currentSettings.machineId.valueOrDefault);
await settingsClient.changeSettings(newSettings); // Save generated machineId to disk

final packageInfo = await PackageInfo.fromPlatform();
var cachedAuth = AuthenticationData.unauthenticated();
try {
cachedAuth = await ApiClient.tryCachedLogin();
} catch (_) {
// Ignore
}

runApp(ReCon(settingsClient: settingsClient, cachedAuthentication: cachedAuth));
runApp(ReCon(settingsClient: settingsClient, cachedAuthentication: cachedAuth, packageInfo: packageInfo));
}

class ReCon extends StatefulWidget {
const ReCon({required this.settingsClient, required this.cachedAuthentication, super.key});
const ReCon({required this.settingsClient, required this.cachedAuthentication, required this.packageInfo, super.key});

final SettingsClient settingsClient;
final AuthenticationData cachedAuthentication;
final PackageInfo packageInfo;

@override
State<ReCon> createState() => _ReConState();
Expand Down Expand Up @@ -120,10 +115,7 @@ class _ReConState extends State<ReCon> {
await showDialog(
context: navigator.overlay!.context,
builder: (context) {
return UpdateNotifier(
remoteVersion: remoteSem,
localVersion: currentSem,
);
return UpdateNotifier(remoteVersion: remoteSem, localVersion: currentSem);
},
);
}
Expand Down Expand Up @@ -156,10 +148,7 @@ class _ReConState extends State<ReCon> {

// Workaround for issue https://github.com/material-foundation/flutter-packages/issues/582
// Dynamic color schemes do not generate new additional surface container colours so we have to do it manually
(ColorScheme light, ColorScheme dark) _generateDynamicColourSchemes(
ColorScheme lightDynamic,
ColorScheme darkDynamic,
) {
(ColorScheme light, ColorScheme dark) _generateDynamicColourSchemes(ColorScheme lightDynamic, ColorScheme darkDynamic) {
final lightBase = ColorScheme.fromSeed(seedColor: lightDynamic.primary);
final darkBase = ColorScheme.fromSeed(seedColor: darkDynamic.primary, brightness: Brightness.dark);

Expand All @@ -173,41 +162,39 @@ class _ReConState extends State<ReCon> {
}

List<Color> _extractAdditionalColours(ColorScheme scheme) => [
scheme.surface,
scheme.surfaceDim,
scheme.surfaceBright,
scheme.surfaceContainerLowest,
scheme.surfaceContainerLow,
scheme.surfaceContainer,
scheme.surfaceContainerHigh,
scheme.surfaceContainerHighest,
];
scheme.surface,
scheme.surfaceDim,
scheme.surfaceBright,
scheme.surfaceContainerLowest,
scheme.surfaceContainerLow,
scheme.surfaceContainer,
scheme.surfaceContainerHigh,
scheme.surfaceContainerHighest,
];

ColorScheme _insertAdditionalColours(ColorScheme scheme, List<Color> additionalColours) => scheme.copyWith(
surface: additionalColours[0],
surfaceDim: additionalColours[1],
surfaceBright: additionalColours[2],
surfaceContainerLowest: additionalColours[3],
surfaceContainerLow: additionalColours[4],
surfaceContainer: additionalColours[5],
surfaceContainerHigh: additionalColours[6],
surfaceContainerHighest: additionalColours[7],
);
surface: additionalColours[0],
surfaceDim: additionalColours[1],
surfaceBright: additionalColours[2],
surfaceContainerLowest: additionalColours[3],
surfaceContainerLow: additionalColours[4],
surfaceContainer: additionalColours[5],
surfaceContainerHigh: additionalColours[6],
surfaceContainerHighest: additionalColours[7],
);

@override
Widget build(BuildContext context) {
return Phoenix(
child: Builder(
builder: (context) {
final apiClient = ApiClient(authenticationData: _authData)
..addLogoutListener(
() {
setState(() {
_authData = AuthenticationData.unauthenticated();
});
Phoenix.rebirth(context);
},
);
final apiClient = ApiClient(authenticationData: _authData, pkgInfo: widget.packageInfo)
..addLogoutListener(() {
setState(() {
_authData = AuthenticationData.unauthenticated();
});
Phoenix.rebirth(context);
});
return ClientHolder(
settingsClient: widget.settingsClient,
apiClient: apiClient,
Expand Down Expand Up @@ -246,21 +233,12 @@ class _ReConState extends State<ReCon> {
),
),
ChangeNotifierProvider(
create: (context) => SessionClient(
apiClient: clientHolder.apiClient,
settingsClient: clientHolder.settingsClient,
),
),
ChangeNotifierProvider(
create: (context) => InventoryClient(
apiClient: clientHolder.apiClient,
),
create: (context) => SessionClient(apiClient: clientHolder.apiClient, settingsClient: clientHolder.settingsClient),
),
ChangeNotifierProvider(create: (context) => InventoryClient(apiClient: clientHolder.apiClient)),
],
child: AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarColor: Theme.of(context).colorScheme.surfaceContainerHighest,
),
value: SystemUiOverlayStyle(statusBarColor: Theme.of(context).colorScheme.surfaceContainerHighest),
child: const Home(),
),
)
Expand Down
13 changes: 13 additions & 0 deletions lib/models/records/asset_chunk.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class AssetChunk {
final int index;
final String key;

const AssetChunk({required this.index, required this.key});

factory AssetChunk.fromMap(Map<String, dynamic> map) => AssetChunk(index: map["index"], key: map["key"]);

Map<String, dynamic> toMap() => {
"index": index,
"key": key,
};
}
Loading