mirror of
https://github.com/inventree/inventree-app.git
synced 2025-04-27 21:16:48 +00:00
Token auth (#434)
* Embed device platform information into token request * Remove username and password from userProfile * Display icon to show if profile has associated user token * Remove username / password from login settings screen * Refactor login procedure around token auth * Refactoring * Add profile login screen - Username / password values are not stored - Just to fetch api token * Login with basic auth * Pass profile to API when connecting * Remove _BASE_URL accessor - Fixes URL caching bug * Add more context to login screen * Add helper functions for unit tests - Change default port to 8000 (makes testing easier with local inventree instance) * api.dart handles basic auth now * fix api_test.dart * Further test improvements * linting fixes * Provide feedback when login fails * More linting * Record user details on login, and display in "about" widget * Fix string lookup * Add extra debug * Fix auth values * Fix user profile test
This commit is contained in:
parent
382c8461f9
commit
76b6191a67
5
.github/workflows/ci.yaml
vendored
5
.github/workflows/ci.yaml
vendored
@ -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
|
||||
|
@ -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
|
||||
---
|
||||
|
383
lib/api.dart
383
lib/api.dart
@ -192,16 +192,13 @@ 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/";
|
||||
|
||||
// Base URL for InvenTree API e.g. http://192.168.120.10:8000
|
||||
String _BASE_URL = "";
|
||||
static const _URL_TOKEN = "user/token/";
|
||||
static const _URL_ROLES = "user/roles/";
|
||||
static const _URL_ME = "user/me/";
|
||||
|
||||
// Accessors for various url endpoints
|
||||
String get baseUrl {
|
||||
String url = _BASE_URL;
|
||||
String url = profile?.server ?? "";
|
||||
|
||||
if (!url.endsWith("/")) {
|
||||
url += "/";
|
||||
@ -242,21 +239,22 @@ class InvenTreeAPI {
|
||||
// Available user roles (permissions) are loaded when connecting to the server
|
||||
Map<String, dynamic> 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 +270,20 @@ class InvenTreeAPI {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Server instance information
|
||||
String instance = "";
|
||||
// Map of user information
|
||||
Map<String, dynamic> userInfo = {};
|
||||
|
||||
// Server version information
|
||||
String _version = "";
|
||||
String get username => (userInfo["username"] ?? "") as String;
|
||||
|
||||
// API version of the connected server
|
||||
int _apiVersion = 1;
|
||||
// Map of server information
|
||||
Map<String, dynamic> serverInfo = {};
|
||||
|
||||
int get apiVersion => _apiVersion;
|
||||
String get serverInstance => (serverInfo["instance"] ?? "") as String;
|
||||
String get serverVersion => (serverInfo["version"] ?? "") as String;
|
||||
int get apiVersion => (serverInfo["apiVersion"] ?? 1) as int;
|
||||
|
||||
// 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 +332,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<InvenTreePlugin> _plugins = [];
|
||||
|
||||
@ -363,9 +358,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,33 +371,68 @@ class InvenTreeAPI {
|
||||
return !isConnected() && _connecting;
|
||||
}
|
||||
|
||||
/*
|
||||
* Connect to the remote InvenTree server:
|
||||
*
|
||||
* - Check that the InvenTree server exists
|
||||
* - Request user token from the server
|
||||
* - Request user roles from the server
|
||||
*/
|
||||
Future<bool> _connect() async {
|
||||
|
||||
if (profile == null) return false;
|
||||
/*
|
||||
* Perform the required login steps, in sequence.
|
||||
* Internal function, called by connectToServer()
|
||||
*
|
||||
* Performs the following steps:
|
||||
*
|
||||
* 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<bool> _connectToServer() async {
|
||||
|
||||
if (!await _checkServer()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!hasToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!await _checkAuth()) {
|
||||
showServerError(_URL_ME, L10().serverNotConnected, L10().serverAuthenticationError);
|
||||
|
||||
// Invalidate the token
|
||||
if (profile != null) {
|
||||
profile!.token = "";
|
||||
await UserProfileDBManager().updateProfile(profile!);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
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<bool> _checkServer() async {
|
||||
|
||||
String address = profile?.server ?? "";
|
||||
String username = profile?.username ?? "";
|
||||
String password = profile?.password ?? "";
|
||||
|
||||
address = address.trim();
|
||||
username = username.trim();
|
||||
password = password.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,
|
||||
success: false
|
||||
L10().incompleteDetails,
|
||||
icon: FontAwesomeIcons.circleExclamation,
|
||||
success: false
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@ -414,27 +441,24 @@ class InvenTreeAPI {
|
||||
address = address + "/";
|
||||
}
|
||||
|
||||
_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;
|
||||
|
||||
// Clear the list of available plugins
|
||||
_plugins.clear();
|
||||
debug("Connecting to ${apiUrl}");
|
||||
|
||||
debug("Connecting to ${apiUrl} -> username=${username}");
|
||||
|
||||
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<String, dynamic> _data = response.asMap();
|
||||
|
||||
// We expect certain response from the server
|
||||
if (!data.containsKey("server") || !data.containsKey("version") || !data.containsKey("instance")) {
|
||||
serverInfo = {..._data};
|
||||
|
||||
if (serverVersion.isEmpty) {
|
||||
showServerError(
|
||||
apiUrl,
|
||||
L10().missingData,
|
||||
@ -444,17 +468,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}";
|
||||
@ -472,18 +488,86 @@ 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;
|
||||
}
|
||||
|
||||
response = await get(_URL_GET_TOKEN);
|
||||
|
||||
/*
|
||||
* Check that the user is authenticated
|
||||
* Fetch the user information
|
||||
*/
|
||||
Future<bool> _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}");
|
||||
if (response.data != null) {
|
||||
debug("Server response: ${response.data.toString()}");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Fetch a token from the server,
|
||||
* with a temporary authentication header
|
||||
*/
|
||||
Future<APIResponse> fetchToken(UserProfile userProfile, String username, String password) async {
|
||||
|
||||
debug("Fetching user token from ${userProfile.server}");
|
||||
|
||||
profile = userProfile;
|
||||
|
||||
// Form a name to request the token with
|
||||
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);
|
||||
}
|
||||
|
||||
// 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,
|
||||
params: { "name": platform_name},
|
||||
headers: { HttpHeaders.authorizationHeader: authHeader}
|
||||
);
|
||||
|
||||
// Invalid response
|
||||
if (!response.successful()) {
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 401:
|
||||
case 403:
|
||||
@ -500,67 +584,29 @@ class InvenTreeAPI {
|
||||
|
||||
debug("Token request failed: STATUS ${response.statusCode}");
|
||||
|
||||
return false;
|
||||
if (response.data != null) {
|
||||
debug("Response data: ${response.data.toString()}");
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
userProfile.token = (data["token"] ?? "") as String;
|
||||
|
||||
if (!result) {
|
||||
showServerError(
|
||||
apiUrl,
|
||||
L10().serverError,
|
||||
L10().errorPluginInfo
|
||||
);
|
||||
debug("Received token from server: ${userProfile.token}");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ok, probably pretty good...
|
||||
|
||||
if (_notification_timer == null) {
|
||||
debug("starting notification timer");
|
||||
_notification_timer = Timer.periodic(
|
||||
Duration(seconds: 5),
|
||||
(timer) {
|
||||
_refreshNotifications();
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
await UserProfileDBManager().updateProfile(userProfile);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
void disconnectFromServer() {
|
||||
@ -568,24 +614,25 @@ class InvenTreeAPI {
|
||||
|
||||
_connected = false;
|
||||
_connecting = false;
|
||||
_token = "";
|
||||
profile = null;
|
||||
|
||||
// Clear received settings
|
||||
_globalSettings.clear();
|
||||
_userSettings.clear();
|
||||
|
||||
serverInfo.clear();
|
||||
_connectionStatusChanged();
|
||||
}
|
||||
|
||||
// Public facing connection function
|
||||
Future<bool> connectToServer() async {
|
||||
|
||||
/* Public facing connection function.
|
||||
*/
|
||||
Future<bool> connectToServer(UserProfile prf) async {
|
||||
|
||||
// Ensure server is first disconnected
|
||||
disconnectFromServer();
|
||||
|
||||
// Load selected profile
|
||||
profile = await UserProfileDBManager().getSelectedProfile();
|
||||
profile = prf;
|
||||
|
||||
if (profile == null) {
|
||||
showSnackIcon(
|
||||
@ -596,12 +643,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) {
|
||||
@ -610,6 +659,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();
|
||||
@ -620,18 +678,13 @@ class InvenTreeAPI {
|
||||
/*
|
||||
* Request the user roles (permissions) from the InvenTree server
|
||||
*/
|
||||
Future<bool> getUserRoles() async {
|
||||
Future<bool> _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;
|
||||
@ -645,12 +698,17 @@ class InvenTreeAPI {
|
||||
|
||||
return true;
|
||||
} else {
|
||||
showServerError(
|
||||
apiUrl,
|
||||
L10().serverError,
|
||||
L10().errorUserRoles,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Request plugin information from the server
|
||||
Future<bool> getPluginInformation() async {
|
||||
Future<bool> _fetchPlugins() async {
|
||||
|
||||
_plugins.clear();
|
||||
|
||||
@ -690,7 +748,7 @@ class InvenTreeAPI {
|
||||
|
||||
if (roles[role] == null) {
|
||||
debug("checkPermission - role '$role' is null!");
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
@ -1045,7 +1103,14 @@ class InvenTreeAPI {
|
||||
* @param method is the HTTP method e.g. "POST" / "PATCH" / "GET" etc;
|
||||
* @param params is the request parameters
|
||||
*/
|
||||
Future<HttpClientRequest?> apiRequest(String url, String method, {Map<String, String> urlParams = const {}}) async {
|
||||
Future<HttpClientRequest?> apiRequest(
|
||||
String url,
|
||||
String method,
|
||||
{
|
||||
Map<String, String> urlParams = const {},
|
||||
Map<String, String> headers = const {},
|
||||
}
|
||||
) async {
|
||||
|
||||
var _url = makeApiUrl(url);
|
||||
|
||||
@ -1085,11 +1150,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()}");
|
||||
@ -1262,12 +1332,13 @@ class InvenTreeAPI {
|
||||
* Perform a HTTP GET request
|
||||
* Returns a json object (or null if did not complete)
|
||||
*/
|
||||
Future<APIResponse> get(String url, {Map<String, String> params = const {}, int? expectedStatusCode=200}) async {
|
||||
Future<APIResponse> get(String url, {Map<String, String> params = const {}, Map<String, String> headers = const {}, int? expectedStatusCode=200}) async {
|
||||
|
||||
HttpClientRequest? request = await apiRequest(
|
||||
url,
|
||||
"GET",
|
||||
urlParams: params,
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
|
||||
@ -1334,7 +1405,10 @@ class InvenTreeAPI {
|
||||
Map<String, String> defaultHeaders() {
|
||||
Map<String, String> 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;
|
||||
@ -1342,11 +1416,10 @@ class InvenTreeAPI {
|
||||
return headers;
|
||||
}
|
||||
|
||||
// Construct a token authorization header
|
||||
String _authorizationHeader() {
|
||||
if (_token.isNotEmpty) {
|
||||
return "Token $_token";
|
||||
} else if (profile != null) {
|
||||
return "Basic " + base64Encode(utf8.encode("${profile?.username}:${profile?.password}"));
|
||||
if (token.isNotEmpty) {
|
||||
return "Token ${token}";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
@ -1579,3 +1652,5 @@ class InvenTreeAPI {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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<List<InvenTreePOLineItem>> getLineItems() async {
|
||||
|
||||
|
@ -44,7 +44,7 @@ Future<Map<String, dynamic>> 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,
|
||||
@ -57,7 +57,8 @@ Future<Map<String, dynamic>> getDeviceInfo() async {
|
||||
|
||||
|
||||
Map<String, dynamic> getServerInfo() => {
|
||||
"version": InvenTreeAPI().version,
|
||||
"version": InvenTreeAPI().serverVersion,
|
||||
"apiVersion": InvenTreeAPI().apiVersion,
|
||||
};
|
||||
|
||||
|
||||
|
@ -591,6 +591,15 @@
|
||||
"locationUpdated": "Stock location updated",
|
||||
"@locationUpdated": {},
|
||||
|
||||
"login": "Login",
|
||||
"@login": {},
|
||||
|
||||
"loginEnter": "Enter login details",
|
||||
"@loginEnter": {},
|
||||
|
||||
"loginEnterDetails": "Username and password are not stored locally",
|
||||
"@loginEnterDetails": {},
|
||||
|
||||
"link": "Link",
|
||||
"@link": {},
|
||||
|
||||
@ -795,6 +804,9 @@
|
||||
"profileDelete": "Delete Server Profile",
|
||||
"@profileDelete": {},
|
||||
|
||||
"profileLogout": "Logout Profile",
|
||||
"@profileLogout": {},
|
||||
|
||||
"profileName": "Profile Name",
|
||||
"@profileName": {},
|
||||
|
||||
|
@ -96,10 +96,18 @@ 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),
|
||||
subtitle: Text(InvenTreeAPI().version.isNotEmpty ? InvenTreeAPI().version : L10().notConnected),
|
||||
subtitle: Text(InvenTreeAPI().serverVersion.isNotEmpty ? InvenTreeAPI().serverVersion : L10().notConnected),
|
||||
leading: FaIcon(FontAwesomeIcons.circleInfo),
|
||||
)
|
||||
);
|
||||
@ -107,13 +115,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),
|
||||
|
@ -1,295 +1,117 @@
|
||||
|
||||
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/user_profile.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/user_profile.dart";
|
||||
import "package:inventree/widget/dialogs.dart";
|
||||
import "package:inventree/widget/progress.dart";
|
||||
|
||||
class InvenTreeLoginSettingsWidget extends StatefulWidget {
|
||||
|
||||
class InvenTreeLoginWidget extends StatefulWidget {
|
||||
|
||||
const InvenTreeLoginWidget(this.profile) : super();
|
||||
|
||||
final UserProfile profile;
|
||||
|
||||
@override
|
||||
_InvenTreeLoginSettingsState createState() => _InvenTreeLoginSettingsState();
|
||||
_InvenTreeLoginState createState() => _InvenTreeLoginState();
|
||||
|
||||
}
|
||||
|
||||
|
||||
class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
||||
|
||||
_InvenTreeLoginSettingsState() {
|
||||
_reload();
|
||||
}
|
||||
|
||||
final GlobalKey<_InvenTreeLoginSettingsState> _loginKey = GlobalKey<_InvenTreeLoginSettingsState>();
|
||||
|
||||
List<UserProfile> profiles = [];
|
||||
|
||||
Future <void> _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 <void> _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 <void> _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<Widget> 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}"),
|
||||
trailing: _getProfileIcon(profile),
|
||||
onTap: () {
|
||||
_selectProfile(context, profile);
|
||||
},
|
||||
onLongPress: () {
|
||||
OneContext().showDialog(
|
||||
builder: (BuildContext context) {
|
||||
return SimpleDialog(
|
||||
title: Text(profile.name),
|
||||
children: <Widget>[
|
||||
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<ProfileEditWidget> {
|
||||
|
||||
_ProfileEditState() : super();
|
||||
class _InvenTreeLoginState extends State<InvenTreeLoginWidget> {
|
||||
|
||||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
String name = "";
|
||||
String server = "";
|
||||
String username = "";
|
||||
String password = "";
|
||||
|
||||
bool _obscured = true;
|
||||
|
||||
String error = "";
|
||||
|
||||
// Attempt login
|
||||
Future<void> _doLogin(BuildContext context) async {
|
||||
|
||||
// Save form
|
||||
formKey.currentState?.save();
|
||||
|
||||
bool valid = formKey.currentState?.validate() ?? false;
|
||||
|
||||
if (valid) {
|
||||
|
||||
// Dismiss the keyboard
|
||||
FocusScopeNode currentFocus = FocusScope.of(context);
|
||||
|
||||
if (!currentFocus.hasPrimaryFocus) {
|
||||
currentFocus.unfocus();
|
||||
}
|
||||
|
||||
showLoadingOverlay(context);
|
||||
|
||||
// Attempt login
|
||||
final response = await InvenTreeAPI().fetchToken(widget.profile, username, password);
|
||||
|
||||
hideLoadingOverlay();
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
List<Widget> 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<Widget> 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(widget.profile == null ? L10().profileAdd : L10().profileEdit),
|
||||
title: Text(L10().login),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.floppyDisk),
|
||||
icon: FaIcon(FontAwesomeIcons.arrowRightToBracket, color: COLOR_SUCCESS),
|
||||
onPressed: () async {
|
||||
if (formKey.currentState!.validate()) {
|
||||
formKey.currentState!.save();
|
||||
|
||||
UserProfile? prf = widget.profile;
|
||||
|
||||
if (prf == null) {
|
||||
UserProfile profile = UserProfile(
|
||||
name: name,
|
||||
server: server,
|
||||
username: username,
|
||||
password: password,
|
||||
);
|
||||
|
||||
await UserProfileDBManager().addProfile(profile);
|
||||
} else {
|
||||
|
||||
prf.name = name;
|
||||
prf.server = server;
|
||||
prf.username = username;
|
||||
prf.password = password;
|
||||
|
||||
await UserProfileDBManager().updateProfile(prf);
|
||||
}
|
||||
|
||||
// Close the window
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
_doLogin(context);
|
||||
},
|
||||
)
|
||||
]
|
||||
@ -302,79 +124,14 @@ class _ProfileEditState extends State<ProfileEditWidget> {
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...before,
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText: L10().profileName,
|
||||
labelStyle: TextStyle(fontWeight: FontWeight.bold),
|
||||
labelText: L10().username,
|
||||
labelStyle: TextStyle(fontWeight: FontWeight.bold),
|
||||
hintText: L10().enterUsername
|
||||
),
|
||||
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]://<server>:<port>",
|
||||
),
|
||||
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;
|
||||
},
|
||||
),
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText: L10().username,
|
||||
labelStyle: TextStyle(fontWeight: FontWeight.bold),
|
||||
hintText: L10().enterUsername
|
||||
),
|
||||
initialValue: widget.profile?.username ?? "",
|
||||
initialValue: "",
|
||||
keyboardType: TextInputType.text,
|
||||
onSaved: (value) {
|
||||
username = value?.trim() ?? "";
|
||||
@ -388,39 +145,41 @@ class _ProfileEditState extends State<ProfileEditWidget> {
|
||||
},
|
||||
),
|
||||
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;
|
||||
});
|
||||
},
|
||||
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;
|
||||
}
|
||||
initialValue: "",
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
obscureText: _obscured,
|
||||
onSaved: (value) {
|
||||
password = value?.trim() ?? "";
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return L10().passwordEmpty;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
)
|
||||
]
|
||||
return null;
|
||||
}
|
||||
),
|
||||
...after,
|
||||
],
|
||||
),
|
||||
padding: EdgeInsets.all(16),
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
430
lib/settings/select_server.dart
Normal file
430
lib/settings/select_server.dart
Normal file
@ -0,0 +1,430 @@
|
||||
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";
|
||||
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
|
||||
_InvenTreeSelectServerState createState() => _InvenTreeSelectServerState();
|
||||
}
|
||||
|
||||
|
||||
class _InvenTreeSelectServerState extends State<InvenTreeSelectServerWidget> {
|
||||
|
||||
_InvenTreeSelectServerState() {
|
||||
_reload();
|
||||
}
|
||||
|
||||
final GlobalKey<_InvenTreeSelectServerState> _loginKey = GlobalKey<_InvenTreeSelectServerState>();
|
||||
|
||||
List<UserProfile> profiles = [];
|
||||
|
||||
Future <void> _reload() async {
|
||||
|
||||
profiles = await UserProfileDBManager().getAllProfiles();
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Logout the selected profile (delete the stored token)
|
||||
*/
|
||||
Future<void> _logoutProfile(BuildContext context, {UserProfile? userProfile}) async {
|
||||
|
||||
if (userProfile != null) {
|
||||
userProfile.token = "";
|
||||
await UserProfileDBManager().updateProfile(userProfile);
|
||||
|
||||
_reload();
|
||||
}
|
||||
|
||||
InvenTreeAPI().disconnectFromServer();
|
||||
_reload();
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Edit the selected profile
|
||||
*/
|
||||
void _editProfile(BuildContext context, {UserProfile? userProfile, bool createNew = false}) {
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ProfileEditWidget(userProfile)
|
||||
)
|
||||
).then((context) {
|
||||
_reload();
|
||||
});
|
||||
}
|
||||
|
||||
Future <void> _selectProfile(BuildContext context, UserProfile profile) async {
|
||||
|
||||
// Disconnect InvenTree
|
||||
InvenTreeAPI().disconnectFromServer();
|
||||
|
||||
var key = profile.key;
|
||||
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await UserProfileDBManager().selectProfile(key);
|
||||
|
||||
UserProfile? prf = await UserProfileDBManager().getProfileByKey(key);
|
||||
|
||||
if (prf == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// First check if the profile has an associate token
|
||||
if (!prf.hasToken) {
|
||||
// Redirect user to login screen
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) => InvenTreeLoginWidget(profile))
|
||||
).then((value) async {
|
||||
_reload();
|
||||
// Reload profile
|
||||
prf = await UserProfileDBManager().getProfileByKey(key);
|
||||
if (prf?.hasToken ?? false) {
|
||||
InvenTreeAPI().connectToServer(prf!).then((result) {
|
||||
_reload();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Exit now, login handled by next widget
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
_reload();
|
||||
|
||||
// Attempt server login (this will load the newly selected profile
|
||||
InvenTreeAPI().connectToServer(prf).then((result) {
|
||||
_reload();
|
||||
});
|
||||
|
||||
_reload();
|
||||
}
|
||||
|
||||
Future <void> _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 null;
|
||||
}
|
||||
|
||||
// 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<Widget> 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: <Widget>[
|
||||
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();
|
||||
_logoutProfile(context, userProfile: profile);
|
||||
},
|
||||
child: ListTile(
|
||||
title: Text(L10().profileLogout),
|
||||
leading: FaIcon(FontAwesomeIcons.userSlash),
|
||||
)
|
||||
),
|
||||
Divider(),
|
||||
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(),
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Widget for editing server details
|
||||
*/
|
||||
class ProfileEditWidget extends StatefulWidget {
|
||||
|
||||
const ProfileEditWidget(this.profile) : super();
|
||||
|
||||
final UserProfile? profile;
|
||||
|
||||
@override
|
||||
_ProfileEditState createState() => _ProfileEditState();
|
||||
}
|
||||
|
||||
class _ProfileEditState extends State<ProfileEditWidget> {
|
||||
|
||||
_ProfileEditState() : super();
|
||||
|
||||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
String name = "";
|
||||
String server = "";
|
||||
|
||||
@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]://<server>:<port>",
|
||||
),
|
||||
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),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -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<InvenTreeSettingsWidget> {
|
||||
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(
|
||||
|
@ -10,20 +10,21 @@ class UserProfile {
|
||||
this.key,
|
||||
this.name = "",
|
||||
this.server = "",
|
||||
this.username = "",
|
||||
this.password = "",
|
||||
this.token = "",
|
||||
this.selected = false,
|
||||
});
|
||||
|
||||
factory UserProfile.fromJson(int key, Map<String, dynamic> json, bool isSelected) => UserProfile(
|
||||
key: key,
|
||||
name: json["name"] as String,
|
||||
server: json["server"] as String,
|
||||
username: json["username"] as String,
|
||||
password: json["password"] 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;
|
||||
|
||||
@ -33,11 +34,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 +45,12 @@ class UserProfile {
|
||||
Map<String, dynamic> 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 +85,7 @@ class UserProfileDBManager {
|
||||
*/
|
||||
Future<bool> 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 +115,7 @@ class UserProfileDBManager {
|
||||
Future<bool> 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 +201,6 @@ class UserProfileDBManager {
|
||||
UserProfile demoProfile = UserProfile(
|
||||
name: "InvenTree Demo",
|
||||
server: "https://demo.inventree.org",
|
||||
username: "allaccess",
|
||||
password: "nolimits",
|
||||
);
|
||||
|
||||
await addProfile(demoProfile);
|
||||
@ -217,6 +212,26 @@ class UserProfileDBManager {
|
||||
return profileList;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Retrieve a profile by key (or null if no match exists)
|
||||
*/
|
||||
Future<UserProfile?> 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)
|
||||
*/
|
||||
|
@ -235,50 +235,9 @@ Future<void> showServerError(String url, String title, String description) async
|
||||
*/
|
||||
Future<void> showStatusCodeError(String url, int status, {String details=""}) async {
|
||||
|
||||
String msg = L10().responseInvalid;
|
||||
String msg = statusCodeToString(status);
|
||||
String extra = url + "\n" + "${L10().statusCode}: ${status}";
|
||||
|
||||
switch (status) {
|
||||
case 400:
|
||||
msg = L10().response400;
|
||||
break;
|
||||
case 401:
|
||||
msg = L10().response401;
|
||||
break;
|
||||
case 403:
|
||||
msg = L10().response403;
|
||||
break;
|
||||
case 404:
|
||||
msg = L10().response404;
|
||||
break;
|
||||
case 405:
|
||||
msg = L10().response405;
|
||||
break;
|
||||
case 429:
|
||||
msg = L10().response429;
|
||||
break;
|
||||
case 500:
|
||||
msg = L10().response500;
|
||||
break;
|
||||
case 501:
|
||||
msg = L10().response501;
|
||||
break;
|
||||
case 502:
|
||||
msg = L10().response502;
|
||||
break;
|
||||
case 503:
|
||||
msg = L10().response503;
|
||||
break;
|
||||
case 504:
|
||||
msg = L10().response504;
|
||||
break;
|
||||
case 505:
|
||||
msg = L10().response505;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (details.isNotEmpty) {
|
||||
extra += "\n";
|
||||
extra += details;
|
||||
@ -292,6 +251,41 @@ Future<void> showStatusCodeError(String url, int status, {String details=""}) as
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Provide a human-readable descriptor for a particular error code
|
||||
*/
|
||||
String statusCodeToString(int status) {
|
||||
switch (status) {
|
||||
case 400:
|
||||
return L10().response400;
|
||||
case 401:
|
||||
return L10().response401;
|
||||
case 403:
|
||||
return L10().response403;
|
||||
case 404:
|
||||
return L10().response404;
|
||||
case 405:
|
||||
return L10().response405;
|
||||
case 429:
|
||||
return L10().response429;
|
||||
case 500:
|
||||
return L10().response500;
|
||||
case 501:
|
||||
return L10().response501;
|
||||
case 502:
|
||||
return L10().response502;
|
||||
case 503:
|
||||
return L10().response503;
|
||||
case 504:
|
||||
return L10().response504;
|
||||
case 505:
|
||||
return L10().response505;
|
||||
default:
|
||||
return L10().responseInvalid + " : ${status}";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Displays a message indicating that the server timed out on a certain request
|
||||
*/
|
||||
|
@ -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<InvenTreeHomePage> with BaseWidgetPr
|
||||
|
||||
void _selectProfile() {
|
||||
Navigator.push(
|
||||
context, MaterialPageRoute(builder: (context) => InvenTreeLoginSettingsWidget())
|
||||
context, MaterialPageRoute(builder: (context) => InvenTreeSelectServerWidget())
|
||||
).then((context) {
|
||||
// Once we return
|
||||
_loadProfile();
|
||||
@ -147,7 +147,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
|
||||
if (!InvenTreeAPI().isConnected() && !InvenTreeAPI().isConnecting()) {
|
||||
|
||||
// Attempt server connection
|
||||
InvenTreeAPI().connectToServer().then((result) {
|
||||
InvenTreeAPI().connectToServer(_profile!).then((result) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
|
@ -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,53 +45,57 @@ void main() {
|
||||
var api = InvenTreeAPI();
|
||||
|
||||
// Incorrect server address
|
||||
var profile = await UserProfileDBManager().getSelectedProfile();
|
||||
var profile = await setupServerProfile();
|
||||
|
||||
assert(profile != null);
|
||||
profile.server = "http://localhost:5555";
|
||||
|
||||
if (profile != null) {
|
||||
profile.server = "http://localhost:5555";
|
||||
await UserProfileDBManager().updateProfile(profile);
|
||||
bool result = await api.connectToServer(profile);
|
||||
assert(!result);
|
||||
|
||||
bool result = await api.connectToServer();
|
||||
assert(!result);
|
||||
debugContains("SocketException at");
|
||||
|
||||
debugContains("SocketException at");
|
||||
// Test incorrect login details
|
||||
profile.server = testServerAddress;
|
||||
|
||||
// Test incorrect login details
|
||||
profile.server = "http://localhost:12345";
|
||||
profile.username = "invalidusername";
|
||||
final response = await api.fetchToken(profile, "baduser", "badpassword");
|
||||
assert(!response.successful());
|
||||
|
||||
await UserProfileDBManager().updateProfile(profile);
|
||||
debugContains("Token request failed");
|
||||
|
||||
await api.connectToServer();
|
||||
assert(!result);
|
||||
assert(!api.checkConnection());
|
||||
|
||||
debugContains("Token request failed");
|
||||
debugContains("Token request failed: STATUS 401");
|
||||
debugContains("showSnackIcon: 'Not Connected'");
|
||||
|
||||
assert(!api.checkConnection());
|
||||
});
|
||||
|
||||
debugContains("Token request failed: STATUS 401");
|
||||
debugContains("showSnackIcon: 'Not Connected'");
|
||||
test("Bad Token", () async {
|
||||
// Test that login fails with a bad token
|
||||
var profile = await setupServerProfile();
|
||||
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
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 +105,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 +114,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"));
|
||||
|
||||
|
@ -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";
|
||||
@ -23,26 +22,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 +71,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!);
|
||||
|
@ -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";
|
||||
|
||||
@ -16,16 +15,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:", () {
|
||||
|
@ -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();
|
||||
@ -20,3 +22,77 @@ void setupTestEnv() {
|
||||
return ".";
|
||||
});
|
||||
}
|
||||
|
||||
// Accessors for default testing values
|
||||
const String testServerAddress = "http://localhost:8000/";
|
||||
const String testServerName = "Test Server";
|
||||
const String testUsername = "testuser";
|
||||
const String testPassword = "testpassword";
|
||||
|
||||
|
||||
/*
|
||||
* Request an API token for the given profile
|
||||
*/
|
||||
Future<bool> fetchProfileToken({
|
||||
UserProfile? profile,
|
||||
String username = testUsername,
|
||||
String password = testPassword
|
||||
}) async {
|
||||
|
||||
profile ??= await UserProfileDBManager().getProfileByName(testServerName);
|
||||
|
||||
assert(profile != null);
|
||||
|
||||
final response = await InvenTreeAPI().fetchToken(profile!, username, password);
|
||||
return response.successful();
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Setup a valid profile, and return it
|
||||
*/
|
||||
Future<UserProfile> 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 && !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<void> 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));
|
||||
}
|
@ -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,20 +60,15 @@ 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));
|
||||
|
||||
// Add a profile with a name that already exists
|
||||
// Add a profile with a new name
|
||||
result = await UserProfileDBManager().addProfile(
|
||||
UserProfile(
|
||||
name: "Test Profile",
|
||||
username: "xyz",
|
||||
password: "hunter42",
|
||||
name: "Another Test Profile",
|
||||
)
|
||||
);
|
||||
|
||||
@ -84,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));
|
||||
});
|
||||
|
||||
@ -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 Server : 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));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user