2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-04-28 05:26:47 +00:00

API refactoring:

- Error if the server version is *older* than the min required version
- Display dialog boxes for different server errors
This commit is contained in:
Oliver Walters 2021-02-02 20:37:54 +11:00
parent cfb3dd6506
commit b69762ff15
8 changed files with 133 additions and 76 deletions

View File

@ -2,11 +2,15 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:image/image.dart'; import 'package:image/image.dart';
import 'package:InvenTree/widget/dialogs.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -22,18 +26,56 @@ import 'package:shared_preferences/shared_preferences.dart';
class InvenTreeAPI { class InvenTreeAPI {
// Minimum supported InvenTree server version is 0.1.1 // Minimum supported InvenTree server version is
static const List<int> MIN_SUPPORTED_VERSION = [0, 1, 1]; static const List<int> MIN_SUPPORTED_VERSION = [0, 1, 5];
String get _requiredVersionString => "${MIN_SUPPORTED_VERSION[0]}.${MIN_SUPPORTED_VERSION[1]}.${MIN_SUPPORTED_VERSION[2]}";
bool _checkServerVersion(String version) { bool _checkServerVersion(String version) {
// TODO - Decode the provided version string and determine if the server is "new" enough
return false; // Provided version string should be of the format "x.y.z [...]"
List<String> versionSplit = version.split(' ').first.split('.');
// Extract the version number <major>.<minor>.<sub> from the string
if (versionSplit.length != 3) {
return false;
}
// Cast the server version to an explicit integer
int server_version_code = 0;
print("server version: ${version}");
server_version_code += (int.tryParse(versionSplit[0]) ?? 0) * 100 * 100;
server_version_code += (int.tryParse(versionSplit[1]) ?? 0) * 100;
server_version_code += (int.tryParse(versionSplit[2]));
print("server version code: ${server_version_code}");
int required_version_code = 0;
required_version_code += MIN_SUPPORTED_VERSION[0] * 100 * 100;
required_version_code += MIN_SUPPORTED_VERSION[1] * 100;
required_version_code += MIN_SUPPORTED_VERSION[2];
print("required version code: ${required_version_code}");
return server_version_code >= required_version_code;
} }
// Endpoint for requesting an API token // Endpoint for requesting an API token
static const _URL_GET_TOKEN = "user/token/"; static const _URL_GET_TOKEN = "user/token/";
static const _URL_GET_VERSION = ""; static const _URL_GET_VERSION = "";
Future<void> showServerError(BuildContext context, String description) async {
showErrorDialog(
context,
I18N.of(context).serverError,
description,
icon: FontAwesomeIcons.server
);
}
// Base URL for InvenTree API e.g. http://192.168.120.10:8000 // Base URL for InvenTree API e.g. http://192.168.120.10:8000
String _BASE_URL = ""; String _BASE_URL = "";
@ -58,21 +100,13 @@ class InvenTreeAPI {
return baseUrl + url; return baseUrl + url;
} }
String get apiUrl { String get apiUrl => _makeUrl("/api/");
return _makeUrl("/api/");
}
String get imageUrl { String get imageUrl => _makeUrl("/image/");
return _makeUrl("/image/");
}
String makeApiUrl(String endpoint) { String makeApiUrl(String endpoint) => _makeUrl("/api/" + endpoint);
return _makeUrl("/api/" + endpoint);
}
String makeUrl(String endpoint) { String makeUrl(String endpoint) => _makeUrl(endpoint);
return _makeUrl(endpoint);
}
String _username = ""; String _username = "";
String _password = ""; String _password = "";
@ -80,9 +114,7 @@ class InvenTreeAPI {
// Authentication token (initially empty, must be requested) // Authentication token (initially empty, must be requested)
String _token = ""; String _token = "";
bool isConnected() { bool isConnected() => _token.isNotEmpty;
return _token.isNotEmpty;
}
/* /*
* Check server connection and display messages if not connected. * Check server connection and display messages if not connected.
@ -106,9 +138,6 @@ class InvenTreeAPI {
return false; return false;
} }
// Is the server version too old?
// TODO
// Finally // Finally
return true; return true;
} }
@ -138,18 +167,17 @@ class InvenTreeAPI {
InvenTreeAPI._internal(); InvenTreeAPI._internal();
Future<bool> connect() async { Future<bool> connect(BuildContext context) async {
var prefs = await SharedPreferences.getInstance(); var prefs = await SharedPreferences.getInstance();
String server = prefs.getString("server"); String server = prefs.getString("server");
String username = prefs.getString("username"); String username = prefs.getString("username");
String password = prefs.getString("password"); String password = prefs.getString("password");
return connectToServer(server, username, password); return connectToServer(context, server, username, password);
} }
Future<bool> connectToServer(String address, String username, Future<bool> connectToServer(BuildContext context, String address, String username, String password) async {
String password) async {
/* Address is the base address for the InvenTree server, /* Address is the base address for the InvenTree server,
* e.g. http://127.0.0.1:8000 * e.g. http://127.0.0.1:8000
@ -161,9 +189,14 @@ class InvenTreeAPI {
username = username.trim(); username = username.trim();
if (address.isEmpty || username.isEmpty || password.isEmpty) { if (address.isEmpty || username.isEmpty || password.isEmpty) {
errorMessage = "Server Error: Empty details supplied"; await showErrorDialog(
print(errorMessage); context,
throw errorMessage; I18N.of(context).error,
"Incomplete server details",
icon: FontAwesomeIcons.server
);
return false;
} }
if (!address.endsWith('/')) { if (!address.endsWith('/')) {
@ -185,29 +218,34 @@ class InvenTreeAPI {
print("Connecting to " + apiUrl + " -> " + username + ":" + password); print("Connecting to " + apiUrl + " -> " + username + ":" + password);
// TODO - Add connection timeout
var response = await get("").timeout(Duration(seconds: 10)).catchError((error) { var response = await get("").timeout(Duration(seconds: 10)).catchError((error) {
if (error is SocketException) { if (error is SocketException) {
print("Could not connect to server"); errorMessage = "Could not connect to server";
return null; return null;
} else if (error is TimeoutException) { } else if (error is TimeoutException) {
print("Server timeout"); errorMessage = "Server timeout";
return null; return null;
} else { } else {
// Unknown error type
errorMessage = error.toString();
// Unknown error type, re-throw error // Unknown error type, re-throw error
print("Unknown error: ${error.toString()}"); return null;
throw error;
} }
}); });
if (response == null) { if (response == null) {
// Null (or error) response: Show dialog and exit
await showServerError(context, errorMessage);
return false; return false;
} }
if (response.statusCode != 200) { if (response.statusCode != 200) {
print("Invalid status code: " + response.statusCode.toString()); // Any status code other than 200!
// TODO: Interpret the error codes and show custom message?
await showServerError(context, "Invalid response code: ${response.statusCode.toString()}");
return false; return false;
} }
@ -217,9 +255,9 @@ class InvenTreeAPI {
// We expect certain response from the server // We expect certain response from the server
if (!data.containsKey("server") || !data.containsKey("version")) { if (!data.containsKey("server") || !data.containsKey("version")) {
errorMessage = "Server resonse contained incorrect data";
print(errorMessage); await showServerError(context, "Server response missing required fields");
throw errorMessage; return false;
} }
print("Server: " + data["server"]); print("Server: " + data["server"]);
@ -228,35 +266,43 @@ class InvenTreeAPI {
_version = data["version"]; _version = data["version"];
if (!_checkServerVersion(_version)) { if (!_checkServerVersion(_version)) {
// TODO - Something? await showServerError(context, "Server version is too old.\n\nServer Version: ${_version}\n\nRequired version: ${_requiredVersionString}");
return false;
} }
// Record the instance name of the server // Record the instance name of the server
instance = data['instance'] ?? ''; instance = data['instance'] ?? '';
// Request token from the server if we do not already have one // Request token from the server if we do not already have one
if (_token.isNotEmpty) { if (false && _token.isNotEmpty) {
print("Already have token - $_token"); print("Already have token - $_token");
return true; return true;
} }
// Clear out the token // Clear the existing token value
_token = ""; _token = "";
print("Requesting token from server");
response = await get(_URL_GET_TOKEN).timeout(Duration(seconds: 10)).catchError((error) { response = await get(_URL_GET_TOKEN).timeout(Duration(seconds: 10)).catchError((error) {
print("Error requesting token:"); print("Error requesting token:");
print(error); print(error);
return false; return null;
}); });
if (response == null) {
await showServerError(context, "Error requesting access token");
return false;
}
if (response.statusCode != 200) { if (response.statusCode != 200) {
print("Invalid status code: " + response.statusCode.toString()); await showServerError(context, "Invalid status code: ${response.statusCode.toString()}");
return false; return false;
} else { } else {
var data = json.decode(response.body); var data = json.decode(response.body);
if (!data.containsKey("token")) { if (!data.containsKey("token")) {
print("No token provided in response"); await showServerError(context, "No token provided in response");
return false; return false;
} }

View File

@ -32,7 +32,6 @@ class BarcodeHandler {
QRViewController _controller; QRViewController _controller;
BuildContext _context; BuildContext _context;
Future<void> onBarcodeMatched(Map<String, dynamic> data) { Future<void> onBarcodeMatched(Map<String, dynamic> data) {
// Called when the server "matches" a barcode // Called when the server "matches" a barcode
// Override this function // Override this function

@ -1 +1 @@
Subproject commit 6b50ee704cacfb3d9b45d89de31b1ce05e94959a Subproject commit 8f8809c18776e8bfac050c35ad17e0416af96ad4

View File

@ -45,9 +45,6 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// Load login details
InvenTreePreferences().loadLoginDetails();
runZoned<Future<void>>(() async { runZoned<Future<void>>(() async {
runApp(InvenTreeApp()); runApp(InvenTreeApp());
}, onError: (error, stackTrace) { }, onError: (error, stackTrace) {
@ -63,6 +60,7 @@ class InvenTreeApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
onGenerateTitle: (BuildContext context) => I18N.of(context).appTitle, onGenerateTitle: (BuildContext context) => I18N.of(context).appTitle,
theme: ThemeData( theme: ThemeData(

View File

@ -1,3 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'api.dart'; import 'api.dart';
@ -35,7 +36,7 @@ class InvenTreePreferences {
InvenTreePreferences._internal(); InvenTreePreferences._internal();
// Load saved login details, and attempt connection // Load saved login details, and attempt connection
void loadLoginDetails() async { void loadLoginDetails(BuildContext context) async {
print("Loading login details"); print("Loading login details");
@ -45,10 +46,10 @@ class InvenTreePreferences {
var username = prefs.getString(_USERNAME) ?? ''; var username = prefs.getString(_USERNAME) ?? '';
var password = prefs.getString(_PASSWORD) ?? ''; var password = prefs.getString(_PASSWORD) ?? '';
await InvenTreeAPI().connectToServer(server, username, password); await InvenTreeAPI().connectToServer(context, server, username, password);
} }
void saveLoginDetails(String server, String username, String password) async { void saveLoginDetails(BuildContext context, String server, String username, String password) async {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
@ -57,6 +58,6 @@ class InvenTreePreferences {
await prefs.setString(_PASSWORD, password); await prefs.setString(_PASSWORD, password);
// Reconnect the API // Reconnect the API
await InvenTreeAPI().connectToServer(server, username, password); await InvenTreeAPI().connectToServer(context, server, username, password);
} }
} }

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../api.dart'; import '../api.dart';
import '../preferences.dart'; import '../preferences.dart';
@ -61,6 +62,15 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
return null; return null;
} }
void _save(BuildContext context) async {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
await InvenTreePreferences().saveLoginDetails(context, _server, _username, _password);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -76,7 +86,7 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
key: _formKey, key: _formKey,
child: new ListView( child: new ListView(
children: <Widget>[ children: <Widget>[
Text("Server Address"), Text(I18N.of(context).serverAddress),
new TextFormField( new TextFormField(
initialValue: _server, initialValue: _server,
decoration: InputDecoration( decoration: InputDecoration(
@ -92,8 +102,8 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
TextFormField( TextFormField(
initialValue: _username, initialValue: _username,
decoration: InputDecoration( decoration: InputDecoration(
hintText: "Username", hintText: I18N.of(context).username,
labelText: "Username", labelText: I18N.of(context).username,
), ),
validator: _validateUsername, validator: _validateUsername,
onSaved: (String value) { onSaved: (String value) {
@ -104,8 +114,8 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
initialValue: _password, initialValue: _password,
obscureText: true, obscureText: true,
decoration: InputDecoration( decoration: InputDecoration(
hintText: "Password", hintText: I18N.of(context).password,
labelText: "Password", labelText: I18N.of(context).password,
), ),
validator: _validatePassword, validator: _validatePassword,
onSaved: (String value) { onSaved: (String value) {
@ -115,8 +125,10 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
Container( Container(
width: screenSize.width, width: screenSize.width,
child: RaisedButton( child: RaisedButton(
child: Text("Save"), child: Text(I18N.of(context).save),
onPressed: this.save, onPressed: () {
_save(context);
}
) )
) )
], ],
@ -125,13 +137,4 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
) )
); );
} }
void save() async {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
await InvenTreePreferences().saveLoginDetails(_server, _username, _password);
}
}
} }

View File

@ -2,6 +2,7 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void showMessage(BuildContext context, String message) { void showMessage(BuildContext context, String message) {
Scaffold.of(context).showSnackBar(SnackBar( Scaffold.of(context).showSnackBar(SnackBar(
@ -9,7 +10,12 @@ void showMessage(BuildContext context, String message) {
)); ));
} }
Future<void> showInfoDialog(BuildContext context, String title, String description, {IconData icon = FontAwesomeIcons.info, String info = "Info", Function onDismissed}) async { Future<void> showInfoDialog(BuildContext context, String title, String description, {IconData icon = FontAwesomeIcons.info, String info, Function onDismissed}) async {
if (info == null || info.isEmpty) {
info = I18N.of(context).info;
}
showDialog( showDialog(
context: context, context: context,
child: SimpleDialog( child: SimpleDialog(
@ -31,7 +37,12 @@ Future<void> showInfoDialog(BuildContext context, String title, String descripti
}); });
} }
Future<void> showErrorDialog(BuildContext context, String title, String description, {IconData icon = FontAwesomeIcons.exclamationCircle, String error = "Error", Function onDismissed}) async { Future<void> showErrorDialog(BuildContext context, String title, String description, {IconData icon = FontAwesomeIcons.exclamationCircle, String error, Function onDismissed}) async {
if (error == null || error.isEmpty) {
error = I18N.of(context).error;
}
showDialog( showDialog(
context: context, context: context,
child: SimpleDialog( child: SimpleDialog(

View File

@ -25,7 +25,6 @@ class InvenTreeHomePage extends StatefulWidget {
class _InvenTreeHomePageState extends State<InvenTreeHomePage> { class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
_InvenTreeHomePageState() : super() { _InvenTreeHomePageState() : super() {
_checkServerConnection();
} }
String _serverAddress = ""; String _serverAddress = "";
@ -63,7 +62,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
/* /*
* Test the server connection * Test the server connection
*/ */
void _checkServerConnection() async { void _checkServerConnection(BuildContext context) async {
var prefs = await SharedPreferences.getInstance(); var prefs = await SharedPreferences.getInstance();
@ -76,7 +75,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
_serverIcon = new FaIcon(FontAwesomeIcons.spinner); _serverIcon = new FaIcon(FontAwesomeIcons.spinner);
_serverStatusColor = Color.fromARGB(255, 50, 50, 250); _serverStatusColor = Color.fromARGB(255, 50, 50, 250);
InvenTreeAPI().connect().then((bool result) { InvenTreeAPI().connect(context).then((bool result) {
if (result) { if (result) {
onConnectSuccess(""); onConnectSuccess("");
@ -322,7 +321,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
leading: _serverIcon, leading: _serverIcon,
onTap: () { onTap: () {
if (!_serverConnection) { if (!_serverConnection) {
_checkServerConnection(); _checkServerConnection(context);
} }
}, },
), ),