From 298cd55eabbf111d1dbb1801417f2529e0a881e6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Oct 2023 20:15:44 +1100 Subject: [PATCH 01/23] Embed device platform information into token request --- lib/api.dart | 37 ++++++++++++++++++++++++++++++++++++- lib/inventree/sentry.dart | 2 +- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index 6ea78805..d8cb654d 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -479,7 +479,42 @@ class InvenTreeAPI { // Clear the existing token value _token = ""; - response = await get(_URL_GET_TOKEN); + // Embed platform information into the token request + // This allows specific tokens to be revoked by the server + String platform_name = "inventree-mobile-app"; + + final deviceInfo = await getDeviceInfo(); + + if (Platform.isAndroid) { + platform_name += "-android"; + } else if (Platform.isIOS) { + platform_name += "-ios"; + } else if (Platform.isMacOS) { + platform_name += "-macos"; + } else if (Platform.isLinux) { + platform_name += "-linux"; + } else if (Platform.isWindows) { + platform_name += "-windows"; + } + + if (deviceInfo.containsKey("name")) { + platform_name += "-" + (deviceInfo["name"] as String); + } + + if (deviceInfo.containsKey("model")) { + platform_name += "-" + (deviceInfo["model"] as String); + } + + if (deviceInfo.containsKey("systemVersion")) { + platform_name += "-" + (deviceInfo["systemVersion"] as String); + } + + response = await get( + _URL_GET_TOKEN, + params: { + "name": platform_name, + } + ); // Invalid response if (!response.successful()) { diff --git a/lib/inventree/sentry.dart b/lib/inventree/sentry.dart index 1d82ff01..3dbfae00 100644 --- a/lib/inventree/sentry.dart +++ b/lib/inventree/sentry.dart @@ -44,7 +44,7 @@ Future> getDeviceInfo() async { "hardware": androidDeviceInfo.hardware, "manufacturer": androidDeviceInfo.manufacturer, "product": androidDeviceInfo.product, - "version": androidDeviceInfo.version.release, + "systemVersion": androidDeviceInfo.version.release, "supported32BitAbis": androidDeviceInfo.supported32BitAbis, "supported64BitAbis": androidDeviceInfo.supported64BitAbis, "supportedAbis": androidDeviceInfo.supportedAbis, From 925def02ed73215b715f250c4a1fe5ae6a969c75 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Oct 2023 20:18:14 +1100 Subject: [PATCH 02/23] Remove username and password from userProfile --- lib/user_profile.dart | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/lib/user_profile.dart b/lib/user_profile.dart index e1b54467..5cb588ef 100644 --- a/lib/user_profile.dart +++ b/lib/user_profile.dart @@ -10,8 +10,7 @@ class UserProfile { this.key, this.name = "", this.server = "", - this.username = "", - this.password = "", + this.token = "", this.selected = false, }); @@ -19,8 +18,7 @@ class UserProfile { key: key, name: json["name"] as String, server: json["server"] as String, - username: json["username"] as String, - password: json["password"] as String, + token: json["token"] as String, selected: isSelected, ); @@ -33,11 +31,8 @@ class UserProfile { // Base address of the InvenTree server String server = ""; - // Username - String username = ""; - - // Password - String password = ""; + // API token + String token = ""; bool selected = false; @@ -47,13 +42,12 @@ class UserProfile { Map toJson() => { "name": name, "server": server, - "username": username, - "password": password, + "token": token, }; @override String toString() { - return "<${key}> ${name} : ${server} - ${username}:${password}"; + return "<${key}> ${name} : ${server}"; } } @@ -88,7 +82,7 @@ class UserProfileDBManager { */ Future addProfile(UserProfile profile) async { - if (profile.name.isEmpty || profile.username.isEmpty || profile.password.isEmpty) { + if (profile.name.isEmpty) { debug("addProfile() : Profile missing required values - not adding to database"); return false; } @@ -118,7 +112,7 @@ class UserProfileDBManager { Future updateProfile(UserProfile profile) async { // Prevent invalid profile data from being updated - if (profile.name.isEmpty || profile.username.isEmpty || profile.password.isEmpty) { + if (profile.name.isEmpty) { debug("updateProfile() : Profile missing required values - not updating"); return false; } @@ -204,8 +198,6 @@ class UserProfileDBManager { UserProfile demoProfile = UserProfile( name: "InvenTree Demo", server: "https://demo.inventree.org", - username: "allaccess", - password: "nolimits", ); await addProfile(demoProfile); From b00778274be4c24745ea930134f8a5ea11eebc9d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Oct 2023 20:26:38 +1100 Subject: [PATCH 03/23] Display icon to show if profile has associated user token --- lib/api.dart | 14 +++++++------- lib/settings/login.dart | 9 +++------ lib/user_profile.dart | 9 ++++++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index d8cb654d..5b3850d0 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -391,17 +391,15 @@ class InvenTreeAPI { if (profile == null) return false; String address = profile?.server ?? ""; - String username = profile?.username ?? ""; - String password = profile?.password ?? ""; + String token = profile?.token ?? ""; address = address.trim(); - username = username.trim(); - password = password.trim(); + token = token.trim(); // Cache the "strictHttps" setting, so we can use it later without async requirement _strictHttps = await InvenTreeSettingsManager().getValue(INV_STRICT_HTTPS, false) as bool; - if (address.isEmpty || username.isEmpty || password.isEmpty) { + if (address.isEmpty) { showSnackIcon( L10().incompleteDetails, icon: FontAwesomeIcons.circleExclamation, @@ -419,7 +417,7 @@ class InvenTreeAPI { // Clear the list of available plugins _plugins.clear(); - debug("Connecting to ${apiUrl} -> username=${username}"); + debug("Connecting to ${apiUrl}"); APIResponse response; @@ -1381,7 +1379,9 @@ class InvenTreeAPI { if (_token.isNotEmpty) { return "Token $_token"; } else if (profile != null) { - return "Basic " + base64Encode(utf8.encode("${profile?.username}:${profile?.password}")); + // TODO: Fix this + return "Basic " + base64Encode(utf8.encode("username:password")); + // return "Basic " + base64Encode(utf8.encode("${profile?.username}:${profile?.password}")); } else { return ""; } diff --git a/lib/settings/login.dart b/lib/settings/login.dart index 185a7797..efb81a1c 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -139,6 +139,7 @@ class _InvenTreeLoginSettingsState extends State { ), tileColor: profile.selected ? Theme.of(context).secondaryHeaderColor : null, subtitle: Text("${profile.server}"), + leading: profile.hasToken ? FaIcon(FontAwesomeIcons.userCheck, color: COLOR_SUCCESS) : FaIcon(FontAwesomeIcons.userSlash, color: COLOR_WARNING), trailing: _getProfileIcon(profile), onTap: () { _selectProfile(context, profile); @@ -272,8 +273,6 @@ class _ProfileEditState extends State { UserProfile profile = UserProfile( name: name, server: server, - username: username, - password: password, ); await UserProfileDBManager().addProfile(profile); @@ -281,8 +280,6 @@ class _ProfileEditState extends State { prf.name = name; prf.server = server; - prf.username = username; - prf.password = password; await UserProfileDBManager().updateProfile(prf); } @@ -374,7 +371,7 @@ class _ProfileEditState extends State { labelStyle: TextStyle(fontWeight: FontWeight.bold), hintText: L10().enterUsername ), - initialValue: widget.profile?.username ?? "", + initialValue: "", //widget.profile?.username ?? "", keyboardType: TextInputType.text, onSaved: (value) { username = value?.trim() ?? ""; @@ -401,7 +398,7 @@ class _ProfileEditState extends State { }, ), ), - initialValue: widget.profile?.password ?? "", + initialValue: "", //widget.profile?.password ?? "", keyboardType: TextInputType.visiblePassword, obscureText: _obscured, onSaved: (value) { diff --git a/lib/user_profile.dart b/lib/user_profile.dart index 5cb588ef..a0234a77 100644 --- a/lib/user_profile.dart +++ b/lib/user_profile.dart @@ -16,12 +16,15 @@ class UserProfile { factory UserProfile.fromJson(int key, Map json, bool isSelected) => UserProfile( key: key, - name: json["name"] as String, - server: json["server"] as String, - token: json["token"] as String, + name: (json["name"] ?? "") as String, + server: (json["server"] ?? "") as String, + token: (json["token"] ?? "") as String, selected: isSelected, ); + // Return true if this profile has a token + bool get hasToken => token.isNotEmpty; + // ID of the profile int? key; From 031a15ff910a24508bb3b2f8afcd0bd3fbed4e33 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 20 Oct 2023 20:42:35 +1100 Subject: [PATCH 04/23] Remove username / password from login settings screen --- lib/settings/login.dart | 49 ----------------------------------------- 1 file changed, 49 deletions(-) diff --git a/lib/settings/login.dart b/lib/settings/login.dart index efb81a1c..d03c2173 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -250,8 +250,6 @@ class _ProfileEditState extends State { String name = ""; String server = ""; - String username = ""; - String password = ""; bool _obscured = true; @@ -365,53 +363,6 @@ class _ProfileEditState extends State { return null; }, ), - TextFormField( - decoration: InputDecoration( - labelText: L10().username, - labelStyle: TextStyle(fontWeight: FontWeight.bold), - hintText: L10().enterUsername - ), - initialValue: "", //widget.profile?.username ?? "", - keyboardType: TextInputType.text, - onSaved: (value) { - username = value?.trim() ?? ""; - }, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return L10().usernameEmpty; - } - - return null; - }, - ), - TextFormField( - decoration: InputDecoration( - labelText: L10().password, - labelStyle: TextStyle(fontWeight: FontWeight.bold), - hintText: L10().enterPassword, - suffixIcon: IconButton( - icon: _obscured ? FaIcon(FontAwesomeIcons.eye) : FaIcon(FontAwesomeIcons.solidEyeSlash), - onPressed: () { - setState(() { - _obscured = !_obscured; - }); - }, - ), - ), - initialValue: "", //widget.profile?.password ?? "", - keyboardType: TextInputType.visiblePassword, - obscureText: _obscured, - onSaved: (value) { - password = value ?? ""; - }, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return L10().passwordEmpty; - } - - return null; - } - ) ] ), padding: EdgeInsets.all(16), From f21eeea9e65cc5e81cdd33e7b556d862e185a72e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 21 Oct 2023 22:17:02 +1100 Subject: [PATCH 05/23] Refactor login procedure around token auth --- lib/api.dart | 325 +++++++++++++++++++++----------------- lib/inventree/sentry.dart | 3 +- lib/settings/about.dart | 6 +- 3 files changed, 188 insertions(+), 146 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index 5b3850d0..7b9a42ce 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -192,9 +192,9 @@ class InvenTreeAPI { bool _strictHttps = false; // Endpoint for requesting an API token - static const _URL_GET_TOKEN = "user/token/"; - - static const _URL_GET_ROLES = "user/roles/"; + static const _URL_TOKEN = "user/token/"; + static const _URL_ROLES = "user/roles/"; + static const _URL_ME = "user/me/"; // Base URL for InvenTree API e.g. http://192.168.120.10:8000 String _BASE_URL = ""; @@ -242,21 +242,22 @@ class InvenTreeAPI { // Available user roles (permissions) are loaded when connecting to the server Map roles = {}; - // Authentication token (initially empty, must be requested) - String _token = ""; + // Profile authentication token + String get token => profile?.token ?? ""; + + bool get hasToken => token.isNotEmpty; String? get serverAddress { return profile?.server; } - bool get hasToken => _token.isNotEmpty; - /* * Check server connection and display messages if not connected. * Useful as a precursor check before performing operations. */ bool checkConnection() { - // Firstly, is the server connected? + + // Is the server connected? if (!isConnected()) { showSnackIcon( @@ -272,16 +273,15 @@ class InvenTreeAPI { return true; } - // Server instance information - String instance = ""; + // Map of server information + Map serverInfo = {}; - // Server version information - String _version = ""; + String get serverInstance => (serverInfo["instance"] ?? "") as String; + String get serverVersion => (serverInfo["version"] ?? "") as String; + int get apiVersion => (serverInfo["apiVersion"] ?? 1) as int; - // API version of the connected server - int _apiVersion = 1; - - int get apiVersion => _apiVersion; + // Plugins enabled at API v34 and above + bool get pluginsEnabled => apiVersion >= 34 && (serverInfo["plugins_enabled"] ?? false) as bool; // API endpoint for receiving purchase order line items was introduced in v12 bool get supportsPoReceive => apiVersion >= 12; @@ -330,13 +330,6 @@ class InvenTreeAPI { bool get supportsBarcodePOReceiveEndpoint => isConnected() && apiVersion >= 139; - // Are plugins enabled on the server? - bool _pluginsEnabled = false; - - // True plugin support requires API v34 or newer - // Returns True only if the server API version is new enough, and plugins are enabled - bool pluginsEnabled() => apiVersion >= 34 && _pluginsEnabled; - // Cached list of plugins (refreshed when we connect to the server) List _plugins = []; @@ -363,9 +356,6 @@ class InvenTreeAPI { // Test if the provided plugin mixin is supported by any active plugins bool supportsMixin(String mixin) => getPlugins(mixin: mixin).isNotEmpty; - // Getter for server version information - String get version => _version; - // Connection status flag - set once connection has been validated bool _connected = false; @@ -379,31 +369,66 @@ class InvenTreeAPI { return !isConnected() && _connecting; } + /* - * Connect to the remote InvenTree server: + * Perform the required login steps, in sequence. + * Internal function, called by connectToServer() + * + * Performs the following steps: * - * - Check that the InvenTree server exists - * - Request user token from the server - * - Request user roles from the server + * 1. Check the api/ endpoint to see if the sever exists + * 2. If no token available, perform user authentication + * 2. Check the api/user/me/ endpoint to see if the user is authenticated + * 3. If not authenticated, purge token, and exit + * 4. Request user roles + * 5. Request information on available plugins */ - Future _connect() async { + Future _connectToServer() async { - if (profile == null) return false; + debug("Connecting to server..."); - String address = profile?.server ?? ""; - String token = profile?.token ?? ""; + if (!await _checkServer()) { + return false; + } + + if (!hasToken) { + // Pass off to manual login procedure + if (!await _doLogin()) { + return false; + } + } - address = address.trim(); - token = token.trim(); + if (!await _checkAuth()) { + showServerError(_URL_ME, L10().serverNotConnected, L10().serverAuthenticationError); + return false; + } - // Cache the "strictHttps" setting, so we can use it later without async requirement - _strictHttps = await InvenTreeSettingsManager().getValue(INV_STRICT_HTTPS, false) as bool; + if (!await _fetchRoles()) { + return false; + } + + if (!await _fetchPlugins()) { + return false; + } + + // Finally, connected + return true; + } + + + /* + * Check that the remote server is available. + * Ping the api/ endpoint, which does not require user authentication + */ + Future _checkServer() async { + + String address = profile?.server ?? ""; if (address.isEmpty) { showSnackIcon( - L10().incompleteDetails, - icon: FontAwesomeIcons.circleExclamation, - success: false + L10().incompleteDetails, + icon: FontAwesomeIcons.circleExclamation, + success: false ); return false; } @@ -412,27 +437,29 @@ class InvenTreeAPI { address = address + "/"; } + // Save the base URL _BASE_URL = address; - // Clear the list of available plugins - _plugins.clear(); + // Cache the "strictHttps" setting, so we can use it later without async requirement + _strictHttps = await InvenTreeSettingsManager().getValue(INV_STRICT_HTTPS, false) as bool; debug("Connecting to ${apiUrl}"); - APIResponse response; - - response = await get("", expectedStatusCode: 200); + APIResponse response = await get("", expectedStatusCode: 200); if (!response.successful()) { + debug("Server returned invalid response: ${response.statusCode}"); showStatusCodeError(apiUrl, response.statusCode, details: response.data.toString()); return false; } - var data = response.asMap(); + Map _data = response.asMap(); - // We expect certain response from the server - if (!data.containsKey("server") || !data.containsKey("version") || !data.containsKey("instance")) { + if (_data is Map) { + serverInfo = {..._data}; + } + if (serverVersion.isEmpty) { showServerError( apiUrl, L10().missingData, @@ -442,17 +469,9 @@ class InvenTreeAPI { return false; } - // Record server information - _version = (data["version"] ?? "") as String; - instance = (data["instance"] ?? "") as String; + if (apiVersion < _minApiVersion) { - // Default API version is 1 if not provided - _apiVersion = (data["apiVersion"] ?? 1) as int; - _pluginsEnabled = (data["plugins_enabled"] ?? false) as bool; - - if (_apiVersion < _minApiVersion) { - - String message = L10().serverApiVersion + ": ${_apiVersion}"; + String message = L10().serverApiVersion + ": ${apiVersion}"; message += "\n"; message += L10().serverApiRequired + ": ${_minApiVersion}"; @@ -470,15 +489,48 @@ class InvenTreeAPI { return false; } - /** - * Request user token information from the server - * This is the stage that we check username:password credentials! - */ - // Clear the existing token value - _token = ""; + // At this point, we have a server which is responding + return true; + } + + + /* + * Login to the server manually, to get an API token + */ + Future _doLogin() async { + + // TODO: Fetch token + + String auth = "Basic " + base64Encode(utf8.encode("admin:inventree")); + + if (!await _fetchToken(auth)) { + showServerError(_URL_TOKEN, L10().tokenError, L10().serverAuthenticationError); + return false; + } + + return true; + } + + /* + * Check that the user is authenticated + * Fetch the user information + */ + Future _checkAuth() async { + debug("Checking user auth @ ${_URL_ME}"); + final response = await get(_URL_ME); + return response.successful() && response.statusCode == 200; + } + + /* + * Fetch a token from the server, + * with a temporary authentication header + */ + Future _fetchToken(String authHeader) async { + if (profile == null) return false; + + debug("Fetching user token"); - // Embed platform information into the token request - // This allows specific tokens to be revoked by the server + // Form a name to request the token with String platform_name = "inventree-mobile-app"; final deviceInfo = await getDeviceInfo(); @@ -507,16 +559,15 @@ class InvenTreeAPI { platform_name += "-" + (deviceInfo["systemVersion"] as String); } - response = await get( - _URL_GET_TOKEN, - params: { - "name": platform_name, - } + // Perform request to get a token + final response = await get( + _URL_TOKEN, + params: { "name": platform_name}, + headers: { HttpHeaders.authorizationHeader: authHeader} ); // Invalid response if (!response.successful()) { - switch (response.statusCode) { case 401: case 403: @@ -536,64 +587,26 @@ class InvenTreeAPI { return false; } - data = response.asMap(); + final data = response.asMap(); if (!data.containsKey("token")) { - showServerError( - apiUrl, - L10().tokenMissing, - L10().tokenMissingFromResponse, - ); - - return false; - } - - // Return the received token - _token = (data["token"] ?? "") as String; - - debug("Received token from server"); - - bool result = false; - - // Request user role information (async) - result = await getUserRoles(); - - if (!result) { showServerError( apiUrl, - L10().serverError, - L10().errorUserRoles, + L10().tokenMissing, + L10().tokenMissingFromResponse, ); return false; } - // Request plugin information (async) - result = await getPluginInformation(); + // Save the token to the user profile + profile?.token = (data["token"] ?? "") as String; - if (!result) { - showServerError( - apiUrl, - L10().serverError, - L10().errorPluginInfo - ); - - return false; - } - - // Ok, probably pretty good... - - if (_notification_timer == null) { - debug("starting notification timer"); - _notification_timer = Timer.periodic( - Duration(seconds: 5), - (timer) { - _refreshNotifications(); - }); - } + debug("Received token from server"); - return true; + bool result = await UserProfileDBManager().updateProfile(profile!); + return result; } void disconnectFromServer() { @@ -601,17 +614,19 @@ class InvenTreeAPI { _connected = false; _connecting = false; - _token = ""; profile = null; // Clear received settings _globalSettings.clear(); _userSettings.clear(); + serverInfo.clear(); _connectionStatusChanged(); } - // Public facing connection function + /* Public facing connection function. + */ + Future connectToServer() async { // Ensure server is first disconnected @@ -629,12 +644,14 @@ class InvenTreeAPI { return false; } - _connecting = true; + // Cancel notification timer + _notification_timer?.cancel(); + _connecting = true; _connectionStatusChanged(); - _connected = await _connect(); - + // Perform the actual connection routine + _connected = await _connectToServer(); _connecting = false; if (_connected) { @@ -643,6 +660,15 @@ class InvenTreeAPI { icon: FontAwesomeIcons.server, success: true, ); + + if (_notification_timer == null) { + debug("starting notification timer"); + _notification_timer = Timer.periodic( + Duration(seconds: 5), + (timer) { + _refreshNotifications(); + }); + } } _connectionStatusChanged(); @@ -653,18 +679,13 @@ class InvenTreeAPI { /* * Request the user roles (permissions) from the InvenTree server */ - Future getUserRoles() async { + Future _fetchRoles() async { roles.clear(); debug("API: Requesting user role data"); - // Next we request the permissions assigned to the current user - // Note: 2021-02-27 this "roles" feature for the API was just introduced. - // Any "older" version of the server allows any API method for any logged in user! - // We will return immediately, but request the user roles in the background - - final response = await get(_URL_GET_ROLES, expectedStatusCode: 200); + final response = await get(_URL_ROLES, expectedStatusCode: 200); if (!response.successful()) { return false; @@ -678,12 +699,17 @@ class InvenTreeAPI { return true; } else { + showServerError( + apiUrl, + L10().serverError, + L10().errorUserRoles, + ); return false; } } // Request plugin information from the server - Future getPluginInformation() async { + Future _fetchPlugins() async { _plugins.clear(); @@ -1078,7 +1104,14 @@ class InvenTreeAPI { * @param method is the HTTP method e.g. "POST" / "PATCH" / "GET" etc; * @param params is the request parameters */ - Future apiRequest(String url, String method, {Map urlParams = const {}}) async { + Future apiRequest( + String url, + String method, + { + Map urlParams = const {}, + Map headers = const {}, + } + ) async { var _url = makeApiUrl(url); @@ -1118,11 +1151,16 @@ class InvenTreeAPI { try { _request = await client.openUrl(method, _uri).timeout(Duration(seconds: 10)); - // Set headers + // Default headers defaultHeaders().forEach((key, value) { _request?.headers.set(key, value); }); + // Custom headers + headers.forEach((key, value) { + _request?.headers.set(key, value); + }); + return _request; } on SocketException catch (error) { debug("SocketException at ${url}: ${error.toString()}"); @@ -1295,12 +1333,13 @@ class InvenTreeAPI { * Perform a HTTP GET request * Returns a json object (or null if did not complete) */ - Future get(String url, {Map params = const {}, int? expectedStatusCode=200}) async { + Future get(String url, {Map params = const {}, Map headers = const {}, int? expectedStatusCode=200}) async { HttpClientRequest? request = await apiRequest( url, "GET", urlParams: params, + headers: headers, ); @@ -1367,7 +1406,10 @@ class InvenTreeAPI { Map defaultHeaders() { Map headers = {}; - headers[HttpHeaders.authorizationHeader] = _authorizationHeader(); + if (hasToken) { + headers[HttpHeaders.authorizationHeader] = _authorizationHeader(); + } + headers[HttpHeaders.acceptHeader] = "application/json"; headers[HttpHeaders.contentTypeHeader] = "application/json"; headers[HttpHeaders.acceptLanguageHeader] = currentLocale; @@ -1375,13 +1417,10 @@ class InvenTreeAPI { return headers; } + // Construct a token authorization header String _authorizationHeader() { - if (_token.isNotEmpty) { - return "Token $_token"; - } else if (profile != null) { - // TODO: Fix this - return "Basic " + base64Encode(utf8.encode("username:password")); - // return "Basic " + base64Encode(utf8.encode("${profile?.username}:${profile?.password}")); + if (token.isNotEmpty) { + return "Token ${token}"; } else { return ""; } @@ -1614,3 +1653,5 @@ class InvenTreeAPI { }); } } + + diff --git a/lib/inventree/sentry.dart b/lib/inventree/sentry.dart index 3dbfae00..4ca9cf41 100644 --- a/lib/inventree/sentry.dart +++ b/lib/inventree/sentry.dart @@ -57,7 +57,8 @@ Future> getDeviceInfo() async { Map getServerInfo() => { - "version": InvenTreeAPI().version, + "version": InvenTreeAPI().serverVersion, + "apiVersion": InvenTreeAPI().apiVersion, }; diff --git a/lib/settings/about.dart b/lib/settings/about.dart index 84578e03..a71205b7 100644 --- a/lib/settings/about.dart +++ b/lib/settings/about.dart @@ -99,7 +99,7 @@ class InvenTreeAboutWidget extends StatelessWidget { tiles.add( ListTile( title: Text(L10().version), - subtitle: Text(InvenTreeAPI().version.isNotEmpty ? InvenTreeAPI().version : L10().notConnected), + subtitle: Text(InvenTreeAPI().serverVersion.isNotEmpty ? InvenTreeAPI().serverVersion : L10().notConnected), leading: FaIcon(FontAwesomeIcons.circleInfo), ) ); @@ -107,13 +107,13 @@ class InvenTreeAboutWidget extends StatelessWidget { tiles.add( ListTile( title: Text(L10().serverInstance), - subtitle: Text(InvenTreeAPI().instance.isNotEmpty ? InvenTreeAPI().instance : L10().notConnected), + subtitle: Text(InvenTreeAPI().serverInstance.isNotEmpty ? InvenTreeAPI().serverInstance : L10().notConnected), leading: FaIcon(FontAwesomeIcons.server), ) ); // Display extra tile if the server supports plugins - if (InvenTreeAPI().pluginsEnabled()) { + if (InvenTreeAPI().pluginsEnabled) { tiles.add( ListTile( title: Text(L10().pluginSupport), From 6bd22395e0530bd1555cb38836702c6eeead9660 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 21 Oct 2023 22:27:49 +1100 Subject: [PATCH 06/23] Refactoring --- lib/api.dart | 8 + lib/settings/login.dart | 375 +------------------------------- lib/settings/select_server.dart | 374 +++++++++++++++++++++++++++++++ lib/settings/settings.dart | 4 +- lib/widget/home.dart | 4 +- 5 files changed, 390 insertions(+), 375 deletions(-) create mode 100644 lib/settings/select_server.dart diff --git a/lib/api.dart b/lib/api.dart index 7b9a42ce..f4c7459d 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -400,6 +400,14 @@ class InvenTreeAPI { if (!await _checkAuth()) { showServerError(_URL_ME, L10().serverNotConnected, L10().serverAuthenticationError); + + + // Invalidate the token + if (profile != null) { + profile!.token = ""; + await UserProfileDBManager().updateProfile(profile!); + } + return false; } diff --git a/lib/settings/login.dart b/lib/settings/login.dart index d03c2173..5bd8ff80 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -1,374 +1,7 @@ -import "package:flutter/material.dart"; -import "package:font_awesome_flutter/font_awesome_flutter.dart"; -import "package:one_context/one_context.dart"; -import "package:inventree/app_colors.dart"; -import "package:inventree/widget/dialogs.dart"; -import "package:inventree/widget/spinner.dart"; -import "package:inventree/l10.dart"; -import "package:inventree/api.dart"; -import "package:inventree/user_profile.dart"; -class InvenTreeLoginSettingsWidget extends StatefulWidget { +/** + * clas + */ - @override - _InvenTreeLoginSettingsState createState() => _InvenTreeLoginSettingsState(); -} - - -class _InvenTreeLoginSettingsState extends State { - - _InvenTreeLoginSettingsState() { - _reload(); - } - - final GlobalKey<_InvenTreeLoginSettingsState> _loginKey = GlobalKey<_InvenTreeLoginSettingsState>(); - - List profiles = []; - - Future _reload() async { - - profiles = await UserProfileDBManager().getAllProfiles(); - - if (!mounted) { - return; - } - - setState(() { - }); - } - - void _editProfile(BuildContext context, {UserProfile? userProfile, bool createNew = false}) { - - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ProfileEditWidget(userProfile) - ) - ).then((context) { - _reload(); - }); - } - - Future _selectProfile(BuildContext context, UserProfile profile) async { - - // Disconnect InvenTree - InvenTreeAPI().disconnectFromServer(); - - var key = profile.key; - - if (key == null) { - return; - } - - await UserProfileDBManager().selectProfile(key); - - if (!mounted) { - return; - } - - _reload(); - - // Attempt server login (this will load the newly selected profile - InvenTreeAPI().connectToServer().then((result) { - _reload(); - }); - - _reload(); - } - - Future _deleteProfile(UserProfile profile) async { - - await UserProfileDBManager().deleteProfile(profile); - - if (!mounted) { - return; - } - - _reload(); - - if (InvenTreeAPI().isConnected() && profile.key == (InvenTreeAPI().profile?.key ?? "")) { - InvenTreeAPI().disconnectFromServer(); - } - } - - Widget? _getProfileIcon(UserProfile profile) { - - // Not selected? No icon for you! - if (!profile.selected) return null; - - // Selected, but (for some reason) not the same as the API... - if ((InvenTreeAPI().profile?.key ?? "") != profile.key) { - return FaIcon( - FontAwesomeIcons.circleQuestion, - color: COLOR_WARNING - ); - } - - // Reflect the connection status of the server - if (InvenTreeAPI().isConnected()) { - return FaIcon( - FontAwesomeIcons.circleCheck, - color: COLOR_SUCCESS - ); - } else if (InvenTreeAPI().isConnecting()) { - return Spinner( - icon: FontAwesomeIcons.spinner, - color: COLOR_PROGRESS, - ); - } else { - return FaIcon( - FontAwesomeIcons.circleXmark, - color: COLOR_DANGER, - ); - } - } - - @override - Widget build(BuildContext context) { - - List children = []; - - if (profiles.isNotEmpty) { - for (int idx = 0; idx < profiles.length; idx++) { - UserProfile profile = profiles[idx]; - - children.add(ListTile( - title: Text( - profile.name, - ), - tileColor: profile.selected ? Theme.of(context).secondaryHeaderColor : null, - subtitle: Text("${profile.server}"), - leading: profile.hasToken ? FaIcon(FontAwesomeIcons.userCheck, color: COLOR_SUCCESS) : FaIcon(FontAwesomeIcons.userSlash, color: COLOR_WARNING), - trailing: _getProfileIcon(profile), - onTap: () { - _selectProfile(context, profile); - }, - onLongPress: () { - OneContext().showDialog( - builder: (BuildContext context) { - return SimpleDialog( - title: Text(profile.name), - children: [ - Divider(), - SimpleDialogOption( - onPressed: () { - Navigator.of(context).pop(); - _selectProfile(context, profile); - }, - child: ListTile( - title: Text(L10().profileConnect), - leading: FaIcon(FontAwesomeIcons.server), - ) - ), - SimpleDialogOption( - onPressed: () { - Navigator.of(context).pop(); - _editProfile(context, userProfile: profile); - }, - child: ListTile( - title: Text(L10().profileEdit), - leading: FaIcon(FontAwesomeIcons.penToSquare) - ) - ), - SimpleDialogOption( - onPressed: () { - Navigator.of(context).pop(); - // Navigator.of(context, rootNavigator: true).pop(); - confirmationDialog( - L10().delete, - L10().profileDelete + "?", - color: Colors.red, - icon: FontAwesomeIcons.trashCan, - onAccept: () { - _deleteProfile(profile); - } - ); - }, - child: ListTile( - title: Text(L10().profileDelete, style: TextStyle(color: Colors.red)), - leading: FaIcon(FontAwesomeIcons.trashCan, color: Colors.red), - ) - ) - ], - ); - } - ); - }, - )); - } - } else { - // No profile available! - children.add( - ListTile( - title: Text(L10().profileNone), - ) - ); - } - - return Scaffold( - key: _loginKey, - appBar: AppBar( - title: Text(L10().profileSelect), - actions: [ - IconButton( - icon: FaIcon(FontAwesomeIcons.circlePlus), - onPressed: () { - _editProfile(context, createNew: true); - }, - ) - ], - ), - body: Container( - child: ListView( - children: ListTile.divideTiles( - context: context, - tiles: children - ).toList(), - ) - ), - ); - } -} - - -class ProfileEditWidget extends StatefulWidget { - - const ProfileEditWidget(this.profile) : super(); - - final UserProfile? profile; - - @override - _ProfileEditState createState() => _ProfileEditState(); -} - -class _ProfileEditState extends State { - - _ProfileEditState() : super(); - - final formKey = GlobalKey(); - - String name = ""; - String server = ""; - - bool _obscured = true; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(widget.profile == null ? L10().profileAdd : L10().profileEdit), - actions: [ - IconButton( - icon: FaIcon(FontAwesomeIcons.floppyDisk), - onPressed: () async { - if (formKey.currentState!.validate()) { - formKey.currentState!.save(); - - UserProfile? prf = widget.profile; - - if (prf == null) { - UserProfile profile = UserProfile( - name: name, - server: server, - ); - - await UserProfileDBManager().addProfile(profile); - } else { - - prf.name = name; - prf.server = server; - - await UserProfileDBManager().updateProfile(prf); - } - - // Close the window - Navigator.of(context).pop(); - } - }, - ) - ] - ), - body: Form( - key: formKey, - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextFormField( - decoration: InputDecoration( - labelText: L10().profileName, - labelStyle: TextStyle(fontWeight: FontWeight.bold), - ), - initialValue: widget.profile?.name ?? "", - maxLines: 1, - keyboardType: TextInputType.text, - onSaved: (value) { - name = value?.trim() ?? ""; - }, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return L10().valueCannotBeEmpty; - } - - return null; - } - ), - TextFormField( - decoration: InputDecoration( - labelText: L10().server, - labelStyle: TextStyle(fontWeight: FontWeight.bold), - hintText: "http[s]://:", - ), - initialValue: widget.profile?.server ?? "", - keyboardType: TextInputType.url, - onSaved: (value) { - server = value?.trim() ?? ""; - }, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return L10().serverEmpty; - } - - value = value.trim(); - - // Spaces are bad - if (value.contains(" ")) { - return L10().invalidHost; - } - - if (!value.startsWith("http:") && !value.startsWith("https:")) { - // return L10().serverStart; - } - - Uri? _uri = Uri.tryParse(value); - - if (_uri == null || _uri.host.isEmpty) { - return L10().invalidHost; - } else { - Uri uri = Uri.parse(value); - - if (uri.hasScheme) { - if (!["http", "https"].contains(uri.scheme.toLowerCase())) { - return L10().serverStart; - } - } else { - return L10().invalidHost; - } - } - - // Everything is OK - return null; - }, - ), - ] - ), - padding: EdgeInsets.all(16), - ), - ) - ); - } - -} \ No newline at end of file +class InvenTreeLoginWidget diff --git a/lib/settings/select_server.dart b/lib/settings/select_server.dart new file mode 100644 index 00000000..46206d0f --- /dev/null +++ b/lib/settings/select_server.dart @@ -0,0 +1,374 @@ +import "package:flutter/material.dart"; +import "package:font_awesome_flutter/font_awesome_flutter.dart"; +import "package:one_context/one_context.dart"; + +import "package:inventree/app_colors.dart"; +import "package:inventree/widget/dialogs.dart"; +import "package:inventree/widget/spinner.dart"; +import "package:inventree/l10.dart"; +import "package:inventree/api.dart"; +import "package:inventree/user_profile.dart"; + +class InvenTreeSelectServerWidget extends StatefulWidget { + + @override + _InvenTreeLoginSettingsState createState() => _InvenTreeLoginSettingsState(); +} + + +class _InvenTreeLoginSettingsState extends State { + + _InvenTreeLoginSettingsState() { + _reload(); + } + + final GlobalKey<_InvenTreeLoginSettingsState> _loginKey = GlobalKey<_InvenTreeLoginSettingsState>(); + + List profiles = []; + + Future _reload() async { + + profiles = await UserProfileDBManager().getAllProfiles(); + + if (!mounted) { + return; + } + + setState(() { + }); + } + + void _editProfile(BuildContext context, {UserProfile? userProfile, bool createNew = false}) { + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ProfileEditWidget(userProfile) + ) + ).then((context) { + _reload(); + }); + } + + Future _selectProfile(BuildContext context, UserProfile profile) async { + + // Disconnect InvenTree + InvenTreeAPI().disconnectFromServer(); + + var key = profile.key; + + if (key == null) { + return; + } + + await UserProfileDBManager().selectProfile(key); + + if (!mounted) { + return; + } + + _reload(); + + // Attempt server login (this will load the newly selected profile + InvenTreeAPI().connectToServer().then((result) { + _reload(); + }); + + _reload(); + } + + Future _deleteProfile(UserProfile profile) async { + + await UserProfileDBManager().deleteProfile(profile); + + if (!mounted) { + return; + } + + _reload(); + + if (InvenTreeAPI().isConnected() && profile.key == (InvenTreeAPI().profile?.key ?? "")) { + InvenTreeAPI().disconnectFromServer(); + } + } + + Widget? _getProfileIcon(UserProfile profile) { + + // Not selected? No icon for you! + if (!profile.selected) return null; + + // Selected, but (for some reason) not the same as the API... + if ((InvenTreeAPI().profile?.key ?? "") != profile.key) { + return FaIcon( + FontAwesomeIcons.circleQuestion, + color: COLOR_WARNING + ); + } + + // Reflect the connection status of the server + if (InvenTreeAPI().isConnected()) { + return FaIcon( + FontAwesomeIcons.circleCheck, + color: COLOR_SUCCESS + ); + } else if (InvenTreeAPI().isConnecting()) { + return Spinner( + icon: FontAwesomeIcons.spinner, + color: COLOR_PROGRESS, + ); + } else { + return FaIcon( + FontAwesomeIcons.circleXmark, + color: COLOR_DANGER, + ); + } + } + + @override + Widget build(BuildContext context) { + + List children = []; + + if (profiles.isNotEmpty) { + for (int idx = 0; idx < profiles.length; idx++) { + UserProfile profile = profiles[idx]; + + children.add(ListTile( + title: Text( + profile.name, + ), + tileColor: profile.selected ? Theme.of(context).secondaryHeaderColor : null, + subtitle: Text("${profile.server}"), + leading: profile.hasToken ? FaIcon(FontAwesomeIcons.userCheck, color: COLOR_SUCCESS) : FaIcon(FontAwesomeIcons.userSlash, color: COLOR_WARNING), + trailing: _getProfileIcon(profile), + onTap: () { + _selectProfile(context, profile); + }, + onLongPress: () { + OneContext().showDialog( + builder: (BuildContext context) { + return SimpleDialog( + title: Text(profile.name), + children: [ + Divider(), + SimpleDialogOption( + onPressed: () { + Navigator.of(context).pop(); + _selectProfile(context, profile); + }, + child: ListTile( + title: Text(L10().profileConnect), + leading: FaIcon(FontAwesomeIcons.server), + ) + ), + SimpleDialogOption( + onPressed: () { + Navigator.of(context).pop(); + _editProfile(context, userProfile: profile); + }, + child: ListTile( + title: Text(L10().profileEdit), + leading: FaIcon(FontAwesomeIcons.penToSquare) + ) + ), + SimpleDialogOption( + onPressed: () { + Navigator.of(context).pop(); + // Navigator.of(context, rootNavigator: true).pop(); + confirmationDialog( + L10().delete, + L10().profileDelete + "?", + color: Colors.red, + icon: FontAwesomeIcons.trashCan, + onAccept: () { + _deleteProfile(profile); + } + ); + }, + child: ListTile( + title: Text(L10().profileDelete, style: TextStyle(color: Colors.red)), + leading: FaIcon(FontAwesomeIcons.trashCan, color: Colors.red), + ) + ) + ], + ); + } + ); + }, + )); + } + } else { + // No profile available! + children.add( + ListTile( + title: Text(L10().profileNone), + ) + ); + } + + return Scaffold( + key: _loginKey, + appBar: AppBar( + title: Text(L10().profileSelect), + actions: [ + IconButton( + icon: FaIcon(FontAwesomeIcons.circlePlus), + onPressed: () { + _editProfile(context, createNew: true); + }, + ) + ], + ), + body: Container( + child: ListView( + children: ListTile.divideTiles( + context: context, + tiles: children + ).toList(), + ) + ), + ); + } +} + + +class ProfileEditWidget extends StatefulWidget { + + const ProfileEditWidget(this.profile) : super(); + + final UserProfile? profile; + + @override + _ProfileEditState createState() => _ProfileEditState(); +} + +class _ProfileEditState extends State { + + _ProfileEditState() : super(); + + final formKey = GlobalKey(); + + String name = ""; + String server = ""; + + bool _obscured = true; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.profile == null ? L10().profileAdd : L10().profileEdit), + actions: [ + IconButton( + icon: FaIcon(FontAwesomeIcons.floppyDisk), + onPressed: () async { + if (formKey.currentState!.validate()) { + formKey.currentState!.save(); + + UserProfile? prf = widget.profile; + + if (prf == null) { + UserProfile profile = UserProfile( + name: name, + server: server, + ); + + await UserProfileDBManager().addProfile(profile); + } else { + + prf.name = name; + prf.server = server; + + await UserProfileDBManager().updateProfile(prf); + } + + // Close the window + Navigator.of(context).pop(); + } + }, + ) + ] + ), + body: Form( + key: formKey, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + decoration: InputDecoration( + labelText: L10().profileName, + labelStyle: TextStyle(fontWeight: FontWeight.bold), + ), + initialValue: widget.profile?.name ?? "", + maxLines: 1, + keyboardType: TextInputType.text, + onSaved: (value) { + name = value?.trim() ?? ""; + }, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return L10().valueCannotBeEmpty; + } + + return null; + } + ), + TextFormField( + decoration: InputDecoration( + labelText: L10().server, + labelStyle: TextStyle(fontWeight: FontWeight.bold), + hintText: "http[s]://:", + ), + initialValue: widget.profile?.server ?? "", + keyboardType: TextInputType.url, + onSaved: (value) { + server = value?.trim() ?? ""; + }, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return L10().serverEmpty; + } + + value = value.trim(); + + // Spaces are bad + if (value.contains(" ")) { + return L10().invalidHost; + } + + if (!value.startsWith("http:") && !value.startsWith("https:")) { + // return L10().serverStart; + } + + Uri? _uri = Uri.tryParse(value); + + if (_uri == null || _uri.host.isEmpty) { + return L10().invalidHost; + } else { + Uri uri = Uri.parse(value); + + if (uri.hasScheme) { + if (!["http", "https"].contains(uri.scheme.toLowerCase())) { + return L10().serverStart; + } + } else { + return L10().invalidHost; + } + } + + // Everything is OK + return null; + }, + ), + ] + ), + padding: EdgeInsets.all(16), + ), + ) + ); + } + +} \ No newline at end of file diff --git a/lib/settings/settings.dart b/lib/settings/settings.dart index 8c420718..dbf7e7e4 100644 --- a/lib/settings/settings.dart +++ b/lib/settings/settings.dart @@ -9,7 +9,7 @@ import "package:inventree/settings/about.dart"; import "package:inventree/settings/app_settings.dart"; import "package:inventree/settings/barcode_settings.dart"; import "package:inventree/settings/home_settings.dart"; -import "package:inventree/settings/login.dart"; +import "package:inventree/settings/select_server.dart"; import "package:inventree/settings/part_settings.dart"; @@ -51,7 +51,7 @@ class _InvenTreeSettingsState extends State { subtitle: Text(L10().configureServer), leading: FaIcon(FontAwesomeIcons.server, color: COLOR_ACTION), onTap: () { - Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeLoginSettingsWidget())); + Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeSelectServerWidget())); }, ), ListTile( diff --git a/lib/widget/home.dart b/lib/widget/home.dart index 5fa8a2e9..563098d6 100644 --- a/lib/widget/home.dart +++ b/lib/widget/home.dart @@ -8,7 +8,7 @@ import "package:inventree/api.dart"; import "package:inventree/app_colors.dart"; import "package:inventree/preferences.dart"; import "package:inventree/l10.dart"; -import "package:inventree/settings/login.dart"; +import "package:inventree/settings/select_server.dart"; import "package:inventree/user_profile.dart"; import "package:inventree/widget/category_display.dart"; @@ -119,7 +119,7 @@ class _InvenTreeHomePageState extends State with BaseWidgetPr void _selectProfile() { Navigator.push( - context, MaterialPageRoute(builder: (context) => InvenTreeLoginSettingsWidget()) + context, MaterialPageRoute(builder: (context) => InvenTreeSelectServerWidget()) ).then((context) { // Once we return _loadProfile(); From 564e80392d71288279f9ae6b97b56d90bbf1d0ec Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 21 Oct 2023 22:48:32 +1100 Subject: [PATCH 07/23] Add profile login screen - Username / password values are not stored - Just to fetch api token --- lib/api.dart | 10 ++- lib/l10n/app_en.arb | 6 ++ lib/settings/login.dart | 107 +++++++++++++++++++++++++++++++- lib/settings/select_server.dart | 44 +++++++++++-- 4 files changed, 161 insertions(+), 6 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index f4c7459d..03d792cf 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -632,9 +632,17 @@ class InvenTreeAPI { _connectionStatusChanged(); } - /* Public facing connection function. + /* + * Check if the selected profile has an API token */ + Future checkHasToken() async { + + final _prf = await UserProfileDBManager().getSelectedProfile(); + return _prf?.hasToken ?? false; + } + /* Public facing connection function. + */ Future connectToServer() async { // Ensure server is first disconnected diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 673d284f..352debd0 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -591,6 +591,9 @@ "locationUpdated": "Stock location updated", "@locationUpdated": {}, + "login": "Login", + "@login": {}, + "link": "Link", "@link": {}, @@ -795,6 +798,9 @@ "profileDelete": "Delete Server Profile", "@profileDelete": {}, + "profileLogout": "Logout Profile", + "@profileLogout": {}, + "profileName": "Profile Name", "@profileName": {}, diff --git a/lib/settings/login.dart b/lib/settings/login.dart index 5bd8ff80..f13d7edf 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -1,7 +1,112 @@ +import "package:flutter/cupertino.dart"; +import "package:flutter/material.dart"; +import "package:font_awesome_flutter/font_awesome_flutter.dart"; +import "package:inventree/user_profile.dart"; +import "package:inventree/l10.dart"; +import "package:inventree/api.dart"; + /** * clas */ -class InvenTreeLoginWidget +class InvenTreeLoginWidget extends StatefulWidget { + + InvenTreeLoginWidget(this.profile); + + UserProfile profile; + + @override + _InvenTreeLoginState createState() => _InvenTreeLoginState(); + +} + + +class _InvenTreeLoginState extends State { + + final formKey = GlobalKey(); + + String username = ""; + String password = ""; + + bool _obscured = true; + + @override + Widget build(BuildContext context) { + + return Scaffold( + appBar: AppBar( + title: Text(L10().login), + ), + body: Form( + key: formKey, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + decoration: InputDecoration( + labelText: L10().username, + labelStyle: TextStyle(fontWeight: FontWeight.bold), + hintText: L10().enterUsername + ), + initialValue: "", + keyboardType: TextInputType.text, + onSaved: (value) { + username = value?.trim() ?? ""; + }, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return L10().usernameEmpty; + } + + return null; + }, + ), + TextFormField( + decoration: InputDecoration( + labelText: L10().password, + labelStyle: TextStyle(fontWeight: FontWeight.bold), + hintText: L10().enterPassword, + suffixIcon: IconButton( + icon: _obscured ? FaIcon(FontAwesomeIcons.eye) : FaIcon(FontAwesomeIcons.solidEyeSlash), + onPressed: () { + setState(() { + _obscured = !_obscured; + }); + }, + ), + ), + initialValue: "", + keyboardType: TextInputType.visiblePassword, + obscureText: _obscured, + onSaved: (value) { + password = value ?? ""; + }, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return L10().passwordEmpty; + } + + return null; + } + ), + Spacer(), + TextButton( + child: Text(L10().login), + onPressed: () { + // TODO: attempt login + }, + ) + ], + ) + ) + ) + ); + + } + +} \ No newline at end of file diff --git a/lib/settings/select_server.dart b/lib/settings/select_server.dart index 46206d0f..0a0d9627 100644 --- a/lib/settings/select_server.dart +++ b/lib/settings/select_server.dart @@ -1,5 +1,6 @@ import "package:flutter/material.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; +import "package:inventree/settings/login.dart"; import "package:one_context/one_context.dart"; import "package:inventree/app_colors.dart"; @@ -12,17 +13,17 @@ import "package:inventree/user_profile.dart"; class InvenTreeSelectServerWidget extends StatefulWidget { @override - _InvenTreeLoginSettingsState createState() => _InvenTreeLoginSettingsState(); + _InvenTreeSelectServerState createState() => _InvenTreeSelectServerState(); } -class _InvenTreeLoginSettingsState extends State { +class _InvenTreeSelectServerState extends State { - _InvenTreeLoginSettingsState() { + _InvenTreeSelectServerState() { _reload(); } - final GlobalKey<_InvenTreeLoginSettingsState> _loginKey = GlobalKey<_InvenTreeLoginSettingsState>(); + final GlobalKey<_InvenTreeSelectServerState> _loginKey = GlobalKey<_InvenTreeSelectServerState>(); List profiles = []; @@ -38,6 +39,23 @@ class _InvenTreeLoginSettingsState extends State { }); } + /* + * Logout the selected profile (delete the stored token) + */ + void _logoutProfile(BuildContext context, {UserProfile? userProfile}) async { + + if (userProfile != null) { + userProfile.token = ""; + await UserProfileDBManager().updateProfile(userProfile!); + + _reload(); + } + + } + + /* + * Edit the selected profile + */ void _editProfile(BuildContext context, {UserProfile? userProfile, bool createNew = false}) { Navigator.push( @@ -63,6 +81,14 @@ class _InvenTreeLoginSettingsState extends State { await UserProfileDBManager().selectProfile(key); + // First check if the profile has an associate token + if (!await InvenTreeAPI().checkHasToken()) { + // Redirect user to login screen + Navigator.push(context, + MaterialPageRoute(builder: (context) => InvenTreeLoginWidget(profile)) + ); + } + if (!mounted) { return; } @@ -171,6 +197,16 @@ class _InvenTreeLoginSettingsState extends State { leading: FaIcon(FontAwesomeIcons.penToSquare) ) ), + SimpleDialogOption( + onPressed: () { + Navigator.of(context).pop(); + _logoutProfile(context, userProfile: profile); + }, + child: ListTile( + title: Text(L10().profileLogout), + leading: FaIcon(FontAwesomeIcons.userSlash), + ) + ), SimpleDialogOption( onPressed: () { Navigator.of(context).pop(); From a4b871a834926ef9631301a48a3ad326d08af8ad Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 21 Oct 2023 23:23:08 +1100 Subject: [PATCH 08/23] Login with basic auth --- lib/api.dart | 30 +++----------------- lib/settings/login.dart | 50 +++++++++++++++++++++++++++------ lib/settings/select_server.dart | 24 ++++++++++++---- 3 files changed, 64 insertions(+), 40 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index 03d792cf..3fd89e1c 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -392,16 +392,12 @@ class InvenTreeAPI { } if (!hasToken) { - // Pass off to manual login procedure - if (!await _doLogin()) { - return false; - } + return false; } if (!await _checkAuth()) { showServerError(_URL_ME, L10().serverNotConnected, L10().serverAuthenticationError); - // Invalidate the token if (profile != null) { profile!.token = ""; @@ -502,23 +498,6 @@ class InvenTreeAPI { } - /* - * Login to the server manually, to get an API token - */ - Future _doLogin() async { - - // TODO: Fetch token - - String auth = "Basic " + base64Encode(utf8.encode("admin:inventree")); - - if (!await _fetchToken(auth)) { - showServerError(_URL_TOKEN, L10().tokenError, L10().serverAuthenticationError); - return false; - } - - return true; - } - /* * Check that the user is authenticated * Fetch the user information @@ -533,8 +512,7 @@ class InvenTreeAPI { * Fetch a token from the server, * with a temporary authentication header */ - Future _fetchToken(String authHeader) async { - if (profile == null) return false; + Future fetchToken(UserProfile userProfile, String authHeader) async { debug("Fetching user token"); @@ -608,11 +586,11 @@ class InvenTreeAPI { } // Save the token to the user profile - profile?.token = (data["token"] ?? "") as String; + userProfile.token = (data["token"] ?? "") as String; debug("Received token from server"); - bool result = await UserProfileDBManager().updateProfile(profile!); + bool result = await UserProfileDBManager().updateProfile(userProfile); return result; } diff --git a/lib/settings/login.dart b/lib/settings/login.dart index f13d7edf..07395bbb 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -1,11 +1,15 @@ +import "dart:convert"; + import "package:flutter/cupertino.dart"; import "package:flutter/material.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; +import "package:inventree/app_colors.dart"; import "package:inventree/user_profile.dart"; import "package:inventree/l10.dart"; import "package:inventree/api.dart"; +import "package:inventree/widget/progress.dart"; /** * clas @@ -32,12 +36,46 @@ class _InvenTreeLoginState extends State { bool _obscured = true; + // Attempt login + Future _doLogin(BuildContext context) async { + + // Save form + formKey.currentState?.save(); + + bool valid = formKey.currentState?.validate() ?? false; + + if (valid) { + + showLoadingOverlay(context); + + // Attempt login + String auth = "Basic " + base64Encode(utf8.encode("${username}:${password}")); + final result = await InvenTreeAPI().fetchToken(widget.profile, auth); + + hideLoadingOverlay(); + + if (result) { + // Return to the server selector screen + Navigator.of(context).pop(); + } + } + + } + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(L10().login), + actions: [ + IconButton( + icon: FaIcon(FontAwesomeIcons.arrowRightToBracket, color: COLOR_SUCCESS), + onPressed: () async { + _doLogin(context); + }, + ) + ] ), body: Form( key: formKey, @@ -84,7 +122,7 @@ class _InvenTreeLoginState extends State { keyboardType: TextInputType.visiblePassword, obscureText: _obscured, onSaved: (value) { - password = value ?? ""; + password = value?.trim() ?? ""; }, validator: (value) { if (value == null || value.trim().isEmpty) { @@ -94,15 +132,9 @@ class _InvenTreeLoginState extends State { return null; } ), - Spacer(), - TextButton( - child: Text(L10().login), - onPressed: () { - // TODO: attempt login - }, - ) ], - ) + ), + padding: EdgeInsets.all(16), ) ) ); diff --git a/lib/settings/select_server.dart b/lib/settings/select_server.dart index 0a0d9627..0e0c7913 100644 --- a/lib/settings/select_server.dart +++ b/lib/settings/select_server.dart @@ -51,6 +51,9 @@ class _InvenTreeSelectServerState extends State { _reload(); } + InvenTreeAPI().disconnectFromServer(); + _reload(); + } /* @@ -86,7 +89,17 @@ class _InvenTreeSelectServerState extends State { // Redirect user to login screen Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeLoginWidget(profile)) - ); + ).then((value) async { + _reload(); + if (await InvenTreeAPI().checkHasToken()) { + InvenTreeAPI().connectToServer().then((result) { + _reload(); + }); + } + }); + + // Exit now, login handled by next widget + return; } if (!mounted) { @@ -125,10 +138,7 @@ class _InvenTreeSelectServerState extends State { // Selected, but (for some reason) not the same as the API... if ((InvenTreeAPI().profile?.key ?? "") != profile.key) { - return FaIcon( - FontAwesomeIcons.circleQuestion, - color: COLOR_WARNING - ); + return null; } // Reflect the connection status of the server @@ -207,6 +217,7 @@ class _InvenTreeSelectServerState extends State { leading: FaIcon(FontAwesomeIcons.userSlash), ) ), + Divider(), SimpleDialogOption( onPressed: () { Navigator.of(context).pop(); @@ -268,6 +279,9 @@ class _InvenTreeSelectServerState extends State { } +/* + * Widget for editing server details + */ class ProfileEditWidget extends StatefulWidget { const ProfileEditWidget(this.profile) : super(); From ecc147e61a631a47feb6f858018cb390937fd5f5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 22 Oct 2023 21:17:04 +1100 Subject: [PATCH 09/23] Pass profile to API when connecting --- assets/release_notes.md | 3 +++ lib/api.dart | 31 +++++++++++++++---------------- lib/settings/login.dart | 11 +++++++---- lib/settings/select_server.dart | 15 +++++++++++---- lib/widget/home.dart | 2 +- 5 files changed, 37 insertions(+), 25 deletions(-) diff --git a/assets/release_notes.md b/assets/release_notes.md index 31e37f51..1bdcb63f 100644 --- a/assets/release_notes.md +++ b/assets/release_notes.md @@ -2,6 +2,9 @@ --- - Add ability to scan in received items using supplier barcodes +- Store API token, rather than username:password +- Ensure that user will lose access if token is revoked by server + ### 0.12.8 - September 2023 --- diff --git a/lib/api.dart b/lib/api.dart index 3fd89e1c..adfe7f64 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -459,9 +459,7 @@ class InvenTreeAPI { Map _data = response.asMap(); - if (_data is Map) { - serverInfo = {..._data}; - } + serverInfo = {..._data}; if (serverVersion.isEmpty) { showServerError( @@ -505,7 +503,17 @@ class InvenTreeAPI { Future _checkAuth() async { debug("Checking user auth @ ${_URL_ME}"); final response = await get(_URL_ME); - return response.successful() && response.statusCode == 200; + + if (response.successful() && response.statusCode == 200) { + return true; + } else { + debug("Auth request failed: Server returned status ${response.statusCode}"); + if (response.data != null) { + debug("Server response: ${response.data.toString()}"); + } + + return false; + } } /* @@ -588,7 +596,7 @@ class InvenTreeAPI { // Save the token to the user profile userProfile.token = (data["token"] ?? "") as String; - debug("Received token from server"); + debug("Received token from server: ${userProfile.token}"); bool result = await UserProfileDBManager().updateProfile(userProfile); @@ -610,24 +618,15 @@ class InvenTreeAPI { _connectionStatusChanged(); } - /* - * Check if the selected profile has an API token - */ - Future checkHasToken() async { - - final _prf = await UserProfileDBManager().getSelectedProfile(); - return _prf?.hasToken ?? false; - } /* Public facing connection function. */ - Future connectToServer() async { + Future connectToServer(UserProfile prf) async { // Ensure server is first disconnected disconnectFromServer(); - // Load selected profile - profile = await UserProfileDBManager().getSelectedProfile(); + profile = prf; if (profile == null) { showSnackIcon( diff --git a/lib/settings/login.dart b/lib/settings/login.dart index 07395bbb..041a7097 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -2,7 +2,6 @@ import "dart:convert"; -import "package:flutter/cupertino.dart"; import "package:flutter/material.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:inventree/app_colors.dart"; @@ -11,9 +10,6 @@ import "package:inventree/l10.dart"; import "package:inventree/api.dart"; import "package:inventree/widget/progress.dart"; -/** - * clas - */ class InvenTreeLoginWidget extends StatefulWidget { @@ -46,6 +42,13 @@ class _InvenTreeLoginState extends State { if (valid) { + // Dismiss the keyboard + FocusScopeNode currentFocus = FocusScope.of(context); + + if (!currentFocus.hasPrimaryFocus) { + currentFocus.unfocus(); + } + showLoadingOverlay(context); // Attempt login diff --git a/lib/settings/select_server.dart b/lib/settings/select_server.dart index 0e0c7913..9eac5ac1 100644 --- a/lib/settings/select_server.dart +++ b/lib/settings/select_server.dart @@ -84,15 +84,22 @@ class _InvenTreeSelectServerState extends State { await UserProfileDBManager().selectProfile(key); + UserProfile? prf = await UserProfileDBManager().getSelectedProfile(); + + if (prf == null) { + return; + } + // First check if the profile has an associate token - if (!await InvenTreeAPI().checkHasToken()) { + if (!prf.hasToken) { // Redirect user to login screen Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeLoginWidget(profile)) ).then((value) async { _reload(); - if (await InvenTreeAPI().checkHasToken()) { - InvenTreeAPI().connectToServer().then((result) { + prf = await UserProfileDBManager().getSelectedProfile(); + if (prf?.hasToken ?? false) { + InvenTreeAPI().connectToServer(prf!).then((result) { _reload(); }); } @@ -109,7 +116,7 @@ class _InvenTreeSelectServerState extends State { _reload(); // Attempt server login (this will load the newly selected profile - InvenTreeAPI().connectToServer().then((result) { + InvenTreeAPI().connectToServer(prf).then((result) { _reload(); }); diff --git a/lib/widget/home.dart b/lib/widget/home.dart index 563098d6..b62daf77 100644 --- a/lib/widget/home.dart +++ b/lib/widget/home.dart @@ -147,7 +147,7 @@ class _InvenTreeHomePageState extends State with BaseWidgetPr if (!InvenTreeAPI().isConnected() && !InvenTreeAPI().isConnecting()) { // Attempt server connection - InvenTreeAPI().connectToServer().then((result) { + InvenTreeAPI().connectToServer(_profile!).then((result) { if (mounted) { setState(() {}); } From 5d1766c8629d034dbf261bd605636762e4a22a6c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 22 Oct 2023 21:31:22 +1100 Subject: [PATCH 10/23] Remove _BASE_URL accessor - Fixes URL caching bug --- lib/api.dart | 14 ++++---------- lib/settings/login.dart | 2 +- lib/settings/select_server.dart | 5 +++-- lib/user_profile.dart | 20 ++++++++++++++++++++ 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index adfe7f64..abfe568a 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -196,12 +196,9 @@ class InvenTreeAPI { static const _URL_ROLES = "user/roles/"; static const _URL_ME = "user/me/"; - // Base URL for InvenTree API e.g. http://192.168.120.10:8000 - String _BASE_URL = ""; - // Accessors for various url endpoints String get baseUrl { - String url = _BASE_URL; + String url = profile?.server ?? ""; if (!url.endsWith("/")) { url += "/"; @@ -385,8 +382,6 @@ class InvenTreeAPI { */ Future _connectToServer() async { - debug("Connecting to server..."); - if (!await _checkServer()) { return false; } @@ -441,9 +436,6 @@ class InvenTreeAPI { address = address + "/"; } - // Save the base URL - _BASE_URL = address; - // Cache the "strictHttps" setting, so we can use it later without async requirement _strictHttps = await InvenTreeSettingsManager().getValue(INV_STRICT_HTTPS, false) as bool; @@ -522,7 +514,9 @@ class InvenTreeAPI { */ Future fetchToken(UserProfile userProfile, String authHeader) async { - debug("Fetching user token"); + debug("Fetching user token from ${userProfile.server}"); + + profile = userProfile; // Form a name to request the token with String platform_name = "inventree-mobile-app"; diff --git a/lib/settings/login.dart b/lib/settings/login.dart index 041a7097..77fffa23 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -13,7 +13,7 @@ import "package:inventree/widget/progress.dart"; class InvenTreeLoginWidget extends StatefulWidget { - InvenTreeLoginWidget(this.profile); + InvenTreeLoginWidget(this.profile) : super(); UserProfile profile; diff --git a/lib/settings/select_server.dart b/lib/settings/select_server.dart index 9eac5ac1..371e4397 100644 --- a/lib/settings/select_server.dart +++ b/lib/settings/select_server.dart @@ -84,7 +84,7 @@ class _InvenTreeSelectServerState extends State { await UserProfileDBManager().selectProfile(key); - UserProfile? prf = await UserProfileDBManager().getSelectedProfile(); + UserProfile? prf = await UserProfileDBManager().getProfileByKey(key); if (prf == null) { return; @@ -97,7 +97,8 @@ class _InvenTreeSelectServerState extends State { MaterialPageRoute(builder: (context) => InvenTreeLoginWidget(profile)) ).then((value) async { _reload(); - prf = await UserProfileDBManager().getSelectedProfile(); + // Reload profile + prf = await UserProfileDBManager().getProfileByKey(key); if (prf?.hasToken ?? false) { InvenTreeAPI().connectToServer(prf!).then((result) { _reload(); diff --git a/lib/user_profile.dart b/lib/user_profile.dart index a0234a77..9d17b065 100644 --- a/lib/user_profile.dart +++ b/lib/user_profile.dart @@ -212,6 +212,26 @@ class UserProfileDBManager { return profileList; } + + /* + * Retrieve a profile by key (or null if no match exists) + */ + Future getProfileByKey(int key) async { + final profiles = await getAllProfiles(); + + UserProfile? prf; + + for (UserProfile profile in profiles) { + if (profile.key == key) { + prf = profile; + break; + } + } + + return prf; + } + + /* * Retrieve a profile by name (or null if no match exists) */ From 00c9ce94a3c3bd1c1585a60f7e3e6e422a2ae15b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 22 Oct 2023 21:41:22 +1100 Subject: [PATCH 11/23] Add more context to login screen --- lib/l10n/app_en.arb | 6 ++++++ lib/settings/login.dart | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 352debd0..4d858d0d 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -594,6 +594,12 @@ "login": "Login", "@login": {}, + "loginEnter": "Enter login details", + "@loginEnter": {}, + + "loginEnterDetails": "Username and password are not stored locally", + "@loginEnterDetails": {}, + "link": "Link", "@link": {}, diff --git a/lib/settings/login.dart b/lib/settings/login.dart index 77fffa23..39fb340a 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -88,6 +88,17 @@ class _InvenTreeLoginState extends State { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ + ListTile( + title: Text(L10().loginEnter), + subtitle: Text(L10().loginEnterDetails), + leading: FaIcon(FontAwesomeIcons.userCheck), + ), + ListTile( + title: Text(L10().server), + subtitle: Text(widget.profile.server), + leading: FaIcon(FontAwesomeIcons.server), + ), + Divider(), TextFormField( decoration: InputDecoration( labelText: L10().username, From 84ceb10dc25fb650933e9a17cf4dddc88f961d8e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 22 Oct 2023 22:44:38 +1100 Subject: [PATCH 12/23] Add helper functions for unit tests - Change default port to 8000 (makes testing easier with local inventree instance) --- .github/workflows/ci.yaml | 5 +--- test/setup.dart | 62 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 02111c89..4e3e3718 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,9 +18,6 @@ env: INVENTREE_ADMIN_USER: testuser INVENTREE_ADMIN_PASSWORD: testpassword INVENTREE_ADMIN_EMAIL: test@test.com - INVENTREE_PYTHON_TEST_SERVER: http://localhost:12345 - INVENTREE_PYTHON_TEST_USERNAME: testuser - INVENTREE_PYTHON_TEST_PASSWORD: testpassword jobs: test: @@ -64,7 +61,7 @@ jobs: invoke install invoke migrate invoke import-fixtures - invoke server -a 127.0.0.1:12345 & + invoke server -a 127.0.0.1:8000 & invoke wait sleep 30 - name: Unit Tests diff --git a/test/setup.dart b/test/setup.dart index 1338f132..bad403d6 100644 --- a/test/setup.dart +++ b/test/setup.dart @@ -1,6 +1,8 @@ import "package:flutter/services.dart"; import "package:flutter_test/flutter_test.dart"; +import "package:inventree/api.dart"; +import "package:inventree/user_profile.dart"; // This is the same as the following issue except it keeps the http client // TestWidgetsFlutterBinding.ensureInitialized(); @@ -19,4 +21,64 @@ void setupTestEnv() { .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return "."; }); +} + +// Accessors for default testing values +const String testServerAddress = "http://localhost:8000"; +const String testServerName = "Test Server"; +const String testUsername = "allaccess"; +const String testPassword = "nolimits"; + + +Future fetchProfileToken({ + UserProfile? profile, + String username = testUsername, + String password = testPassword +}) async { + + if (profile == null) { + profile = await UserProfileDBManager().getProfileByName(testServerName); + } + + assert(profile != null); + + bool result = await InvenTreeAPI().fetchToken(profile!, username, password); + return result; +} + + +/* + * Setup a valid profile, and return it + */ +Future setupServerProfile({bool select = true, bool fetchToken = false}) async { + // Setup a valid server profile + + UserProfile? profile = await UserProfileDBManager().getProfileByName(testServerName); + + if (profile == null) { + // Profile does not already exist - create it! + bool result = await UserProfileDBManager().addProfile( + UserProfile( + server: testServerAddress, + name: testServerName + ) + ); + + assert(result); + } + + profile = await UserProfileDBManager().getProfileByName(testServerName); + assert(profile != null); + + if (select) { + assert(await UserProfileDBManager().selectProfileByName(testServerName)); + } + + if (fetchToken) { + final bool result = await fetchProfileToken(profile: profile); + assert(result); + assert(profile!.hasToken); + } + + return profile!; } \ No newline at end of file From cf25918d3707f3e8ba658ee9892b6529ccf146a8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 22 Oct 2023 22:44:55 +1100 Subject: [PATCH 13/23] api.dart handles basic auth now --- lib/api.dart | 7 +++++-- lib/settings/login.dart | 3 +-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index abfe568a..d293e9a7 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -512,7 +512,7 @@ class InvenTreeAPI { * Fetch a token from the server, * with a temporary authentication header */ - Future fetchToken(UserProfile userProfile, String authHeader) async { + Future fetchToken(UserProfile userProfile, String username, String password) async { debug("Fetching user token from ${userProfile.server}"); @@ -547,6 +547,9 @@ class InvenTreeAPI { platform_name += "-" + (deviceInfo["systemVersion"] as String); } + // Construct auth header from username and password + String authHeader = "Basic " + base64Encode(utf8.encode("${username}:${password}")); + // Perform request to get a token final response = await get( _URL_TOKEN, @@ -736,7 +739,7 @@ class InvenTreeAPI { if (roles[role] == null) { debug("checkPermission - role '$role' is null!"); - return true; + return false; } try { diff --git a/lib/settings/login.dart b/lib/settings/login.dart index 39fb340a..720a19fc 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -52,8 +52,7 @@ class _InvenTreeLoginState extends State { showLoadingOverlay(context); // Attempt login - String auth = "Basic " + base64Encode(utf8.encode("${username}:${password}")); - final result = await InvenTreeAPI().fetchToken(widget.profile, auth); + final result = await InvenTreeAPI().fetchToken(widget.profile, username, password); hideLoadingOverlay(); From dde764f029792deff2e0174d745f1f83d7f47825 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 22 Oct 2023 23:12:59 +1100 Subject: [PATCH 14/23] fix api_test.dart --- test/api_test.dart | 75 ++++++++++++++++++++-------------------------- test/setup.dart | 2 +- 2 files changed, 33 insertions(+), 44 deletions(-) diff --git a/test/api_test.dart b/test/api_test.dart index 5990d161..1213a5c5 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -17,37 +17,11 @@ void main() { setUp(() async { - if (! await UserProfileDBManager().profileNameExists("Test Profile")) { - // Create and select a profile to user - - print("TEST: Creating profile for user 'testuser'"); - - await UserProfileDBManager().addProfile(UserProfile( - name: "Test Profile", - server: "http://localhost:12345", - username: "testuser", - password: "testpassword", - selected: true, - )); - } - - var prf = await UserProfileDBManager().getSelectedProfile(); - - // Ensure that the server settings are correct by default, - // as they can get overwritten by subsequent tests - - if (prf != null) { - prf.name = "Test Profile"; - prf.server = "http://localhost:12345"; - prf.username = "testuser"; - prf.password = "testpassword"; - - await UserProfileDBManager().updateProfile(prf); - } + await setupServerProfile(select: true); // Ensure the profile is selected assert(! await UserProfileDBManager().selectProfileByName("Missing Profile")); - assert(await UserProfileDBManager().selectProfileByName("Test Profile")); + assert(await UserProfileDBManager().selectProfileByName(testServerName)); }); @@ -71,26 +45,22 @@ void main() { var api = InvenTreeAPI(); // Incorrect server address - var profile = await UserProfileDBManager().getSelectedProfile(); + var profile = await setupServerProfile(); assert(profile != null); if (profile != null) { profile.server = "http://localhost:5555"; - await UserProfileDBManager().updateProfile(profile); - bool result = await api.connectToServer(); + bool result = await api.connectToServer(profile); assert(!result); debugContains("SocketException at"); // Test incorrect login details - profile.server = "http://localhost:12345"; - profile.username = "invalidusername"; + profile.server = testServerAddress; - await UserProfileDBManager().updateProfile(profile); - - await api.connectToServer(); + result = await api.fetchToken(profile, "baduser", "badpassword"); assert(!result); debugContains("Token request failed"); @@ -106,18 +76,33 @@ void main() { }); + test("Bad Token", () async { + // Test that login fails with a bad token + var profile = await setupServerProfile(); + + profile.token = "bad-token"; + + bool result = await InvenTreeAPI().connectToServer(profile); + assert(!result); + }); + test("Login Success", () async { // Test that we can login to the server successfully var api = InvenTreeAPI(); - // Attempt to connect - final bool result = await api.connectToServer(); + final profile = await setupServerProfile(select: true, fetchToken: true); + assert(profile.hasToken); + + // Now, connect to the server + bool result = await api.connectToServer(profile!); // Check expected values assert(result); assert(api.hasToken); - expect(api.baseUrl, equals("http://localhost:12345/")); + expect(api.baseUrl, equals(testServerAddress)); + + assert(api.hasToken); assert(api.isConnected()); assert(!api.isConnecting()); assert(api.checkConnection()); @@ -127,7 +112,8 @@ void main() { // Test server version information var api = InvenTreeAPI(); - assert(await api.connectToServer()); + final profile = await setupServerProfile(fetchToken: true); + assert(await api.connectToServer(profile)); // Check supported functions assert(api.apiVersion >= 50); @@ -135,12 +121,15 @@ void main() { assert(api.supportsNotifications); assert(api.supportsPoReceive); - // Ensure we can request (and receive) user roles - assert(await api.getUserRoles()); + assert(api.serverInstance.isNotEmpty); + assert(api.serverVersion.isNotEmpty); + + // Ensure we can have user role data + assert(api.roles.isNotEmpty); // Check available permissions assert(api.checkPermission("part", "change")); - assert(api.checkPermission("stocklocation", "delete")); + assert(api.checkPermission("stock_location", "delete")); assert(!api.checkPermission("part", "weirdpermission")); assert(api.checkPermission("blah", "bloo")); diff --git a/test/setup.dart b/test/setup.dart index bad403d6..6ed29d04 100644 --- a/test/setup.dart +++ b/test/setup.dart @@ -24,7 +24,7 @@ void setupTestEnv() { } // Accessors for default testing values -const String testServerAddress = "http://localhost:8000"; +const String testServerAddress = "http://localhost:8000/"; const String testServerName = "Test Server"; const String testUsername = "allaccess"; const String testPassword = "nolimits"; From e4e0bc80366d3bb07a1b26f20a71ba0f6bc2b4c4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 22 Oct 2023 23:26:13 +1100 Subject: [PATCH 15/23] Further test improvements --- test/barcode_test.dart | 23 ++--------------------- test/models_test.dart | 11 +---------- test/setup.dart | 15 ++++++++++++++- test/user_profile_test.dart | 26 ++++++-------------------- 4 files changed, 23 insertions(+), 52 deletions(-) diff --git a/test/barcode_test.dart b/test/barcode_test.dart index e28715b3..3e57d86a 100644 --- a/test/barcode_test.dart +++ b/test/barcode_test.dart @@ -23,26 +23,7 @@ void main() { // Connect to the server setUpAll(() async { - final prf = await UserProfileDBManager().getProfileByName("Test Profile"); - - if (prf != null) { - await UserProfileDBManager().deleteProfile(prf); - } - - bool result = await UserProfileDBManager().addProfile( - UserProfile( - name: "Test Profile", - server: "http://localhost:12345", - username: "testuser", - password: "testpassword", - selected: true, - ), - ); - - assert(result); - - assert(await UserProfileDBManager().selectProfileByName("Test Profile")); - assert(await InvenTreeAPI().connectToServer()); + await connectToTestServer(); }); setUp(() async { @@ -91,8 +72,8 @@ void main() { test("Scan Into Location", () async { final item = await InvenTreeStockItem().get(1) as InvenTreeStockItem?; - assert(item != null); + assert(item!.pk == 1); var handler = StockItemScanIntoLocationHandler(item!); diff --git a/test/models_test.dart b/test/models_test.dart index c44ad5cd..4bc425c8 100644 --- a/test/models_test.dart +++ b/test/models_test.dart @@ -16,16 +16,7 @@ void main() { setupTestEnv(); setUp(() async { - await UserProfileDBManager().addProfile(UserProfile( - name: "Test Profile", - server: "http://localhost:12345", - username: "testuser", - password: "testpassword", - selected: true, - )); - - assert(await UserProfileDBManager().selectProfileByName("Test Profile")); - assert(await InvenTreeAPI().connectToServer()); + await connectToTestServer(); }); group("Category Tests:", () { diff --git a/test/setup.dart b/test/setup.dart index 6ed29d04..78a145df 100644 --- a/test/setup.dart +++ b/test/setup.dart @@ -74,11 +74,24 @@ Future setupServerProfile({bool select = true, bool fetchToken = fa assert(await UserProfileDBManager().selectProfileByName(testServerName)); } - if (fetchToken) { + if (fetchToken && !profile!.hasToken) { final bool result = await fetchProfileToken(profile: profile); assert(result); assert(profile!.hasToken); } return profile!; +} + + +/* + * Complete all steps necessary to login to the server + */ +Future connectToTestServer() async { + + // Setup profile, and fetch user token as necessary + final profile = await setupServerProfile(fetchToken: true); + + // Connect to the server + assert(await InvenTreeAPI().connectToServer(profile)); } \ No newline at end of file diff --git a/test/user_profile_test.dart b/test/user_profile_test.dart index 1da201fa..e1f68b3e 100644 --- a/test/user_profile_test.dart +++ b/test/user_profile_test.dart @@ -27,10 +27,8 @@ void main() { // Now, create one! bool result = await UserProfileDBManager().addProfile(UserProfile( - name: "Test Profile", - username: "testuser", - password: "testpassword""", - server: "http://localhost:12345", + name: testServerName, + server: testServerAddress, selected: true, )); @@ -62,10 +60,7 @@ void main() { test("Add Invalid Profiles", () async { // Add a profile with missing data bool result = await UserProfileDBManager().addProfile( - UserProfile( - username: "what", - password: "why", - ) + UserProfile() ); expect(result, equals(false)); @@ -74,8 +69,6 @@ void main() { result = await UserProfileDBManager().addProfile( UserProfile( name: "Test Profile", - username: "xyz", - password: "hunter42", ) ); @@ -104,23 +97,16 @@ void main() { if (prf != null) { UserProfile p = prf; - expect(p.name, equals("Test Profile")); - expect(p.username, equals("testuser")); - expect(p.password, equals("testpassword")); - expect(p.server, equals("http://localhost:12345")); + expect(p.name, equals(testServerName)); + expect(p.server, equals(testServerAddress)); - expect(p.toString(), equals("<${p.key}> Test Profile : http://localhost:12345 - testuser:testpassword")); + expect(p.toString(), equals("<${p.key}> Test Profile : http://localhost:8000")); // Test that we can update the profile p.name = "different name"; bool result = await UserProfileDBManager().updateProfile(p); expect(result, equals(true)); - - // Trying to update with an invalid value will fail! - p.password = ""; - result = await UserProfileDBManager().updateProfile(p); - expect(result, equals(false)); } }); }); From d313c3a3d1aa15c2fedfaf6878c2277d731624b4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 22 Oct 2023 23:31:04 +1100 Subject: [PATCH 16/23] linting fixes --- lib/settings/login.dart | 5 +---- lib/settings/select_server.dart | 6 ++---- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/settings/login.dart b/lib/settings/login.dart index 720a19fc..306c64ad 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -1,7 +1,4 @@ - -import "dart:convert"; - import "package:flutter/material.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:inventree/app_colors.dart"; @@ -15,7 +12,7 @@ class InvenTreeLoginWidget extends StatefulWidget { InvenTreeLoginWidget(this.profile) : super(); - UserProfile profile; + final UserProfile profile; @override _InvenTreeLoginState createState() => _InvenTreeLoginState(); diff --git a/lib/settings/select_server.dart b/lib/settings/select_server.dart index 371e4397..cd247b67 100644 --- a/lib/settings/select_server.dart +++ b/lib/settings/select_server.dart @@ -42,11 +42,11 @@ class _InvenTreeSelectServerState extends State { /* * Logout the selected profile (delete the stored token) */ - void _logoutProfile(BuildContext context, {UserProfile? userProfile}) async { + Future _logoutProfile(BuildContext context, {UserProfile? userProfile}) async { if (userProfile != null) { userProfile.token = ""; - await UserProfileDBManager().updateProfile(userProfile!); + await UserProfileDBManager().updateProfile(userProfile); _reload(); } @@ -309,8 +309,6 @@ class _ProfileEditState extends State { String name = ""; String server = ""; - bool _obscured = true; - @override Widget build(BuildContext context) { return Scaffold( From 119fdf021225df81b14ed437ca3101a882d0217b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 22 Oct 2023 23:48:59 +1100 Subject: [PATCH 17/23] Provide feedback when login fails --- lib/api.dart | 10 ++---- lib/settings/login.dart | 57 ++++++++++++++++++++++++++-------- lib/widget/dialogs.dart | 68 +++++++++++++++++++---------------------- test/api_test.dart | 4 +-- test/setup.dart | 4 +-- 5 files changed, 82 insertions(+), 61 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index d293e9a7..2e78081d 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -512,7 +512,7 @@ class InvenTreeAPI { * Fetch a token from the server, * with a temporary authentication header */ - Future fetchToken(UserProfile userProfile, String username, String password) async { + Future fetchToken(UserProfile userProfile, String username, String password) async { debug("Fetching user token from ${userProfile.server}"); @@ -574,8 +574,6 @@ class InvenTreeAPI { } debug("Token request failed: STATUS ${response.statusCode}"); - - return false; } final data = response.asMap(); @@ -586,8 +584,6 @@ class InvenTreeAPI { L10().tokenMissing, L10().tokenMissingFromResponse, ); - - return false; } // Save the token to the user profile @@ -595,9 +591,9 @@ class InvenTreeAPI { debug("Received token from server: ${userProfile.token}"); - bool result = await UserProfileDBManager().updateProfile(userProfile); + await UserProfileDBManager().updateProfile(userProfile); - return result; + return response; } void disconnectFromServer() { diff --git a/lib/settings/login.dart b/lib/settings/login.dart index 306c64ad..1396e3c3 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -5,6 +5,7 @@ import "package:inventree/app_colors.dart"; import "package:inventree/user_profile.dart"; import "package:inventree/l10.dart"; import "package:inventree/api.dart"; +import "package:inventree/widget/dialogs.dart"; import "package:inventree/widget/progress.dart"; @@ -29,6 +30,8 @@ class _InvenTreeLoginState extends State { bool _obscured = true; + String error = ""; + // Attempt login Future _doLogin(BuildContext context) async { @@ -49,13 +52,26 @@ class _InvenTreeLoginState extends State { showLoadingOverlay(context); // Attempt login - final result = await InvenTreeAPI().fetchToken(widget.profile, username, password); + final response = await InvenTreeAPI().fetchToken(widget.profile, username, password); hideLoadingOverlay(); - if (result) { + if (response.successful()) { // Return to the server selector screen Navigator.of(context).pop(); + } else { + var data = response.asMap(); + + String err; + + if (data.containsKey("detail")) { + err = (data["detail"] ?? "") as String; + } else { + err = statusCodeToString(response.statusCode); + } + setState(() { + error = err; + }); } } @@ -64,6 +80,30 @@ class _InvenTreeLoginState extends State { @override Widget build(BuildContext context) { + List before = [ + ListTile( + title: Text(L10().loginEnter), + subtitle: Text(L10().loginEnterDetails), + leading: FaIcon(FontAwesomeIcons.userCheck), + ), + ListTile( + title: Text(L10().server), + subtitle: Text(widget.profile.server), + leading: FaIcon(FontAwesomeIcons.server), + ), + Divider(), + ]; + + List after = []; + + if (error.isNotEmpty) { + after.add(Divider()); + after.add(ListTile( + leading: FaIcon(FontAwesomeIcons.circleExclamation, color: COLOR_DANGER), + title: Text(L10().error, style: TextStyle(color: COLOR_DANGER)), + subtitle: Text(error, style: TextStyle(color: COLOR_DANGER)), + )); + } return Scaffold( appBar: AppBar( title: Text(L10().login), @@ -84,17 +124,7 @@ class _InvenTreeLoginState extends State { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - ListTile( - title: Text(L10().loginEnter), - subtitle: Text(L10().loginEnterDetails), - leading: FaIcon(FontAwesomeIcons.userCheck), - ), - ListTile( - title: Text(L10().server), - subtitle: Text(widget.profile.server), - leading: FaIcon(FontAwesomeIcons.server), - ), - Divider(), + ...before, TextFormField( decoration: InputDecoration( labelText: L10().username, @@ -142,6 +172,7 @@ class _InvenTreeLoginState extends State { return null; } ), + ...after, ], ), padding: EdgeInsets.all(16), diff --git a/lib/widget/dialogs.dart b/lib/widget/dialogs.dart index 1c86ef2f..88229c13 100644 --- a/lib/widget/dialogs.dart +++ b/lib/widget/dialogs.dart @@ -235,60 +235,54 @@ Future showServerError(String url, String title, String description) async */ Future showStatusCodeError(String url, int status, {String details=""}) async { - String msg = L10().responseInvalid; + String msg = statusCodeToString(status); String extra = url + "\n" + "${L10().statusCode}: ${status}"; + if (details.isNotEmpty) { + extra += "\n"; + extra += details; + } + + showServerError( + url, + msg, + extra, + ); +} + + +/* + * Provide a human-readable descriptor for a particular error code + */ +String statusCodeToString(int status) { switch (status) { case 400: - msg = L10().response400; - break; + return L10().response400; case 401: - msg = L10().response401; - break; + return L10().response401; case 403: - msg = L10().response403; - break; + return L10().response403; case 404: - msg = L10().response404; - break; + return L10().response404; case 405: - msg = L10().response405; - break; + return L10().response405; case 429: - msg = L10().response429; - break; + return L10().response429; case 500: - msg = L10().response500; - break; + return L10().response500; case 501: - msg = L10().response501; - break; + return L10().response501; case 502: - msg = L10().response502; - break; + return L10().response502; case 503: - msg = L10().response503; - break; + return L10().response503; case 504: - msg = L10().response504; - break; + return L10().response504; case 505: - msg = L10().response505; - break; + return L10().response505; default: - break; - } - - if (details.isNotEmpty) { - extra += "\n"; - extra += details; + return L10().responseInvalid + " : ${status}"; } - - showServerError( - url, - msg, - extra, - ); } diff --git a/test/api_test.dart b/test/api_test.dart index 1213a5c5..82797636 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -60,8 +60,8 @@ void main() { // Test incorrect login details profile.server = testServerAddress; - result = await api.fetchToken(profile, "baduser", "badpassword"); - assert(!result); + final response = await api.fetchToken(profile, "baduser", "badpassword"); + assert(!response.successful()); debugContains("Token request failed"); diff --git a/test/setup.dart b/test/setup.dart index 78a145df..3f8c7e5d 100644 --- a/test/setup.dart +++ b/test/setup.dart @@ -42,8 +42,8 @@ Future fetchProfileToken({ assert(profile != null); - bool result = await InvenTreeAPI().fetchToken(profile!, username, password); - return result; + final response = await InvenTreeAPI().fetchToken(profile!, username, password); + return response.successful(); } From 9be578dbea5693ef5253ef0d248da75019f6c1f8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 22 Oct 2023 23:52:27 +1100 Subject: [PATCH 18/23] More linting --- lib/settings/login.dart | 2 +- test/api_test.dart | 33 +++++++++++++-------------------- test/barcode_test.dart | 1 - test/models_test.dart | 1 - test/setup.dart | 9 +++++---- 5 files changed, 19 insertions(+), 27 deletions(-) diff --git a/lib/settings/login.dart b/lib/settings/login.dart index 1396e3c3..57b1c161 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -11,7 +11,7 @@ import "package:inventree/widget/progress.dart"; class InvenTreeLoginWidget extends StatefulWidget { - InvenTreeLoginWidget(this.profile) : super(); + const InvenTreeLoginWidget(this.profile) : super(); final UserProfile profile; diff --git a/test/api_test.dart b/test/api_test.dart index 82797636..a9c72910 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -47,32 +47,25 @@ void main() { // Incorrect server address var profile = await setupServerProfile(); - assert(profile != null); + profile.server = "http://localhost:5555"; - if (profile != null) { - profile.server = "http://localhost:5555"; - - bool result = await api.connectToServer(profile); - assert(!result); - - debugContains("SocketException at"); + bool result = await api.connectToServer(profile); + assert(!result); - // Test incorrect login details - profile.server = testServerAddress; + debugContains("SocketException at"); - final response = await api.fetchToken(profile, "baduser", "badpassword"); - assert(!response.successful()); + // Test incorrect login details + profile.server = testServerAddress; - debugContains("Token request failed"); + final response = await api.fetchToken(profile, "baduser", "badpassword"); + assert(!response.successful()); - assert(!api.checkConnection()); + debugContains("Token request failed"); - debugContains("Token request failed: STATUS 401"); - debugContains("showSnackIcon: 'Not Connected'"); + assert(!api.checkConnection()); - } else { - assert(false); - } + debugContains("Token request failed: STATUS 401"); + debugContains("showSnackIcon: 'Not Connected'"); }); @@ -94,7 +87,7 @@ void main() { assert(profile.hasToken); // Now, connect to the server - bool result = await api.connectToServer(profile!); + bool result = await api.connectToServer(profile); // Check expected values assert(result); diff --git a/test/barcode_test.dart b/test/barcode_test.dart index 3e57d86a..181e74d2 100644 --- a/test/barcode_test.dart +++ b/test/barcode_test.dart @@ -10,7 +10,6 @@ import "package:flutter_test/flutter_test.dart"; import "package:inventree/api.dart"; import "package:inventree/barcode/barcode.dart"; import "package:inventree/helpers.dart"; -import "package:inventree/user_profile.dart"; import "package:inventree/inventree/part.dart"; import "package:inventree/inventree/stock.dart"; diff --git a/test/models_test.dart b/test/models_test.dart index 4bc425c8..5a82d474 100644 --- a/test/models_test.dart +++ b/test/models_test.dart @@ -5,7 +5,6 @@ import "package:test/test.dart"; import "package:inventree/api.dart"; -import "package:inventree/user_profile.dart"; import "package:inventree/inventree/model.dart"; import "package:inventree/inventree/part.dart"; diff --git a/test/setup.dart b/test/setup.dart index 3f8c7e5d..98ded725 100644 --- a/test/setup.dart +++ b/test/setup.dart @@ -30,15 +30,16 @@ const String testUsername = "allaccess"; const String testPassword = "nolimits"; +/* + * Request an API token for the given profile + */ Future fetchProfileToken({ UserProfile? profile, String username = testUsername, String password = testPassword }) async { - if (profile == null) { - profile = await UserProfileDBManager().getProfileByName(testServerName); - } + profile ??= await UserProfileDBManager().getProfileByName(testServerName); assert(profile != null); @@ -77,7 +78,7 @@ Future setupServerProfile({bool select = true, bool fetchToken = fa if (fetchToken && !profile!.hasToken) { final bool result = await fetchProfileToken(profile: profile); assert(result); - assert(profile!.hasToken); + assert(profile.hasToken); } return profile!; From 1d99ed7d0affee9dcef1d78f487fd735b1d8f49f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 23 Oct 2023 00:06:45 +1100 Subject: [PATCH 19/23] Record user details on login, and display in "about" widget --- lib/api.dart | 9 +++++++++ lib/settings/about.dart | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/lib/api.dart b/lib/api.dart index 2e78081d..9e995d14 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -270,6 +270,11 @@ class InvenTreeAPI { return true; } + // Map of user information + Map userInfo = {}; + + String get username => (userInfo["username"] ?? "") as String; + // Map of server information Map serverInfo = {}; @@ -494,9 +499,13 @@ class InvenTreeAPI { */ Future _checkAuth() async { debug("Checking user auth @ ${_URL_ME}"); + + userInfo.clear(); + final response = await get(_URL_ME); if (response.successful() && response.statusCode == 200) { + userInfo = response.asMap(); return true; } else { debug("Auth request failed: Server returned status ${response.statusCode}"); diff --git a/lib/settings/about.dart b/lib/settings/about.dart index a71205b7..3f02f6fc 100644 --- a/lib/settings/about.dart +++ b/lib/settings/about.dart @@ -96,6 +96,14 @@ class InvenTreeAboutWidget extends StatelessWidget { ) ); + tiles.add( + ListTile( + title: Text(L10().username), + subtitle: Text(InvenTreeAPI().username), + leading: InvenTreeAPI().username.isNotEmpty ? FaIcon(FontAwesomeIcons.user) : FaIcon(FontAwesomeIcons.userSlash, color: COLOR_DANGER), + ) + ); + tiles.add( ListTile( title: Text(L10().version), From 92412716eb61edf7887ba2385453f593ec6b5613 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 23 Oct 2023 00:34:28 +1100 Subject: [PATCH 20/23] Fix string lookup --- lib/inventree/purchase_order.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/inventree/purchase_order.dart b/lib/inventree/purchase_order.dart index 199bc15e..defb9f8a 100644 --- a/lib/inventree/purchase_order.dart +++ b/lib/inventree/purchase_order.dart @@ -131,7 +131,17 @@ class InvenTreePurchaseOrder extends InvenTreeModel { } } - String get totalPriceCurrency => getString("total_price_currency"); + // Return the currency for this order + // Note that the nomenclature in the API changed at some point + String get totalPriceCurrency { + if (jsondata.containsKey("order_currency")) { + return getString("order_currency"); + } else if (jsondata.containsKey("total_price_currency")) { + return getString("total_price_currency"); + } else { + return ""; + } + } Future> getLineItems() async { From e088c8fd44e9c6ad1162d9b849fcbccd3914e105 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 23 Oct 2023 00:55:04 +1100 Subject: [PATCH 21/23] Add extra debug --- lib/api.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/api.dart b/lib/api.dart index 9e995d14..b8a2d7b1 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -583,6 +583,10 @@ class InvenTreeAPI { } debug("Token request failed: STATUS ${response.statusCode}"); + + if (response.data != null) { + debug("Response data: ${response.data.toString()}"); + } } final data = response.asMap(); From 256fd04e66feaba6028a09d846975702fd390b3a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 23 Oct 2023 01:11:05 +1100 Subject: [PATCH 22/23] Fix auth values --- test/setup.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/setup.dart b/test/setup.dart index 98ded725..b9785b7d 100644 --- a/test/setup.dart +++ b/test/setup.dart @@ -26,8 +26,8 @@ void setupTestEnv() { // Accessors for default testing values const String testServerAddress = "http://localhost:8000/"; const String testServerName = "Test Server"; -const String testUsername = "allaccess"; -const String testPassword = "nolimits"; +const String testUsername = "testuser"; +const String testPassword = "testpassword"; /* From 64230a6d06624dcf2a125776c78b0c02b97957b8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 23 Oct 2023 01:20:24 +1100 Subject: [PATCH 23/23] Fix user profile test --- test/user_profile_test.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/user_profile_test.dart b/test/user_profile_test.dart index e1f68b3e..0921d761 100644 --- a/test/user_profile_test.dart +++ b/test/user_profile_test.dart @@ -65,10 +65,10 @@ void main() { expect(result, equals(false)); - // Add a profile with a name that already exists + // Add a profile with a new name result = await UserProfileDBManager().addProfile( UserProfile( - name: "Test Profile", + name: "Another Test Profile", ) ); @@ -77,14 +77,14 @@ void main() { // Check that the number of protocols available is still the same var profiles = await UserProfileDBManager().getAllProfiles(); - expect(profiles.length, equals(1)); + expect(profiles.length, equals(2)); }); test("Profile Name Check", () async { bool result = await UserProfileDBManager().profileNameExists("doesnotexist"); expect(result, equals(false)); - result = await UserProfileDBManager().profileNameExists("Test Profile"); + result = await UserProfileDBManager().profileNameExists("Test Server"); expect(result, equals(true)); }); @@ -100,7 +100,7 @@ void main() { expect(p.name, equals(testServerName)); expect(p.server, equals(testServerAddress)); - expect(p.toString(), equals("<${p.key}> Test Profile : http://localhost:8000")); + expect(p.toString(), equals("<${p.key}> Test Server : http://localhost:8000/")); // Test that we can update the profile p.name = "different name";