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_USER: testuser
|
||||||
INVENTREE_ADMIN_PASSWORD: testpassword
|
INVENTREE_ADMIN_PASSWORD: testpassword
|
||||||
INVENTREE_ADMIN_EMAIL: test@test.com
|
INVENTREE_ADMIN_EMAIL: test@test.com
|
||||||
INVENTREE_PYTHON_TEST_SERVER: http://localhost:12345
|
|
||||||
INVENTREE_PYTHON_TEST_USERNAME: testuser
|
|
||||||
INVENTREE_PYTHON_TEST_PASSWORD: testpassword
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@ -64,7 +61,7 @@ jobs:
|
|||||||
invoke install
|
invoke install
|
||||||
invoke migrate
|
invoke migrate
|
||||||
invoke import-fixtures
|
invoke import-fixtures
|
||||||
invoke server -a 127.0.0.1:12345 &
|
invoke server -a 127.0.0.1:8000 &
|
||||||
invoke wait
|
invoke wait
|
||||||
sleep 30
|
sleep 30
|
||||||
- name: Unit Tests
|
- name: Unit Tests
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
- Add ability to scan in received items using supplier barcodes
|
- 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
|
### 0.12.8 - September 2023
|
||||||
---
|
---
|
||||||
|
383
lib/api.dart
383
lib/api.dart
@ -192,16 +192,13 @@ class InvenTreeAPI {
|
|||||||
bool _strictHttps = false;
|
bool _strictHttps = false;
|
||||||
|
|
||||||
// Endpoint for requesting an API token
|
// Endpoint for requesting an API token
|
||||||
static const _URL_GET_TOKEN = "user/token/";
|
static const _URL_TOKEN = "user/token/";
|
||||||
|
static const _URL_ROLES = "user/roles/";
|
||||||
static const _URL_GET_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
|
// Accessors for various url endpoints
|
||||||
String get baseUrl {
|
String get baseUrl {
|
||||||
String url = _BASE_URL;
|
String url = profile?.server ?? "";
|
||||||
|
|
||||||
if (!url.endsWith("/")) {
|
if (!url.endsWith("/")) {
|
||||||
url += "/";
|
url += "/";
|
||||||
@ -242,21 +239,22 @@ class InvenTreeAPI {
|
|||||||
// Available user roles (permissions) are loaded when connecting to the server
|
// Available user roles (permissions) are loaded when connecting to the server
|
||||||
Map<String, dynamic> roles = {};
|
Map<String, dynamic> roles = {};
|
||||||
|
|
||||||
// Authentication token (initially empty, must be requested)
|
// Profile authentication token
|
||||||
String _token = "";
|
String get token => profile?.token ?? "";
|
||||||
|
|
||||||
|
bool get hasToken => token.isNotEmpty;
|
||||||
|
|
||||||
String? get serverAddress {
|
String? get serverAddress {
|
||||||
return profile?.server;
|
return profile?.server;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get hasToken => _token.isNotEmpty;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Check server connection and display messages if not connected.
|
* Check server connection and display messages if not connected.
|
||||||
* Useful as a precursor check before performing operations.
|
* Useful as a precursor check before performing operations.
|
||||||
*/
|
*/
|
||||||
bool checkConnection() {
|
bool checkConnection() {
|
||||||
// Firstly, is the server connected?
|
|
||||||
|
// Is the server connected?
|
||||||
if (!isConnected()) {
|
if (!isConnected()) {
|
||||||
|
|
||||||
showSnackIcon(
|
showSnackIcon(
|
||||||
@ -272,16 +270,20 @@ class InvenTreeAPI {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server instance information
|
// Map of user information
|
||||||
String instance = "";
|
Map<String, dynamic> userInfo = {};
|
||||||
|
|
||||||
// Server version information
|
String get username => (userInfo["username"] ?? "") as String;
|
||||||
String _version = "";
|
|
||||||
|
|
||||||
// API version of the connected server
|
// Map of server information
|
||||||
int _apiVersion = 1;
|
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
|
// API endpoint for receiving purchase order line items was introduced in v12
|
||||||
bool get supportsPoReceive => apiVersion >= 12;
|
bool get supportsPoReceive => apiVersion >= 12;
|
||||||
@ -330,13 +332,6 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
bool get supportsBarcodePOReceiveEndpoint => isConnected() && apiVersion >= 139;
|
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)
|
// Cached list of plugins (refreshed when we connect to the server)
|
||||||
List<InvenTreePlugin> _plugins = [];
|
List<InvenTreePlugin> _plugins = [];
|
||||||
|
|
||||||
@ -363,9 +358,6 @@ class InvenTreeAPI {
|
|||||||
// Test if the provided plugin mixin is supported by any active plugins
|
// Test if the provided plugin mixin is supported by any active plugins
|
||||||
bool supportsMixin(String mixin) => getPlugins(mixin: mixin).isNotEmpty;
|
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
|
// Connection status flag - set once connection has been validated
|
||||||
bool _connected = false;
|
bool _connected = false;
|
||||||
|
|
||||||
@ -379,33 +371,68 @@ class InvenTreeAPI {
|
|||||||
return !isConnected() && _connecting;
|
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 address = profile?.server ?? "";
|
||||||
String username = profile?.username ?? "";
|
|
||||||
String password = profile?.password ?? "";
|
|
||||||
|
|
||||||
address = address.trim();
|
if (address.isEmpty) {
|
||||||
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) {
|
|
||||||
showSnackIcon(
|
showSnackIcon(
|
||||||
L10().incompleteDetails,
|
L10().incompleteDetails,
|
||||||
icon: FontAwesomeIcons.circleExclamation,
|
icon: FontAwesomeIcons.circleExclamation,
|
||||||
success: false
|
success: false
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -414,27 +441,24 @@ class InvenTreeAPI {
|
|||||||
address = address + "/";
|
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
|
debug("Connecting to ${apiUrl}");
|
||||||
_plugins.clear();
|
|
||||||
|
|
||||||
debug("Connecting to ${apiUrl} -> username=${username}");
|
APIResponse response = await get("", expectedStatusCode: 200);
|
||||||
|
|
||||||
APIResponse response;
|
|
||||||
|
|
||||||
response = await get("", expectedStatusCode: 200);
|
|
||||||
|
|
||||||
if (!response.successful()) {
|
if (!response.successful()) {
|
||||||
|
debug("Server returned invalid response: ${response.statusCode}");
|
||||||
showStatusCodeError(apiUrl, response.statusCode, details: response.data.toString());
|
showStatusCodeError(apiUrl, response.statusCode, details: response.data.toString());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var data = response.asMap();
|
Map<String, dynamic> _data = response.asMap();
|
||||||
|
|
||||||
// We expect certain response from the server
|
serverInfo = {..._data};
|
||||||
if (!data.containsKey("server") || !data.containsKey("version") || !data.containsKey("instance")) {
|
|
||||||
|
|
||||||
|
if (serverVersion.isEmpty) {
|
||||||
showServerError(
|
showServerError(
|
||||||
apiUrl,
|
apiUrl,
|
||||||
L10().missingData,
|
L10().missingData,
|
||||||
@ -444,17 +468,9 @@ class InvenTreeAPI {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record server information
|
if (apiVersion < _minApiVersion) {
|
||||||
_version = (data["version"] ?? "") as String;
|
|
||||||
instance = (data["instance"] ?? "") as String;
|
|
||||||
|
|
||||||
// Default API version is 1 if not provided
|
String message = L10().serverApiVersion + ": ${apiVersion}";
|
||||||
_apiVersion = (data["apiVersion"] ?? 1) as int;
|
|
||||||
_pluginsEnabled = (data["plugins_enabled"] ?? false) as bool;
|
|
||||||
|
|
||||||
if (_apiVersion < _minApiVersion) {
|
|
||||||
|
|
||||||
String message = L10().serverApiVersion + ": ${_apiVersion}";
|
|
||||||
|
|
||||||
message += "\n";
|
message += "\n";
|
||||||
message += L10().serverApiRequired + ": ${_minApiVersion}";
|
message += L10().serverApiRequired + ": ${_minApiVersion}";
|
||||||
@ -472,18 +488,86 @@ class InvenTreeAPI {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// At this point, we have a server which is responding
|
||||||
* Request user token information from the server
|
return true;
|
||||||
* This is the stage that we check username:password credentials!
|
}
|
||||||
*/
|
|
||||||
// Clear the existing token value
|
|
||||||
_token = "";
|
|
||||||
|
|
||||||
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
|
// Invalid response
|
||||||
if (!response.successful()) {
|
if (!response.successful()) {
|
||||||
|
|
||||||
switch (response.statusCode) {
|
switch (response.statusCode) {
|
||||||
case 401:
|
case 401:
|
||||||
case 403:
|
case 403:
|
||||||
@ -500,67 +584,29 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
debug("Token request failed: STATUS ${response.statusCode}");
|
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")) {
|
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(
|
showServerError(
|
||||||
apiUrl,
|
apiUrl,
|
||||||
L10().serverError,
|
L10().tokenMissing,
|
||||||
L10().errorUserRoles,
|
L10().tokenMissingFromResponse,
|
||||||
);
|
);
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request plugin information (async)
|
// Save the token to the user profile
|
||||||
result = await getPluginInformation();
|
userProfile.token = (data["token"] ?? "") as String;
|
||||||
|
|
||||||
if (!result) {
|
debug("Received token from server: ${userProfile.token}");
|
||||||
showServerError(
|
|
||||||
apiUrl,
|
|
||||||
L10().serverError,
|
|
||||||
L10().errorPluginInfo
|
|
||||||
);
|
|
||||||
|
|
||||||
return false;
|
await UserProfileDBManager().updateProfile(userProfile);
|
||||||
}
|
|
||||||
|
|
||||||
// Ok, probably pretty good...
|
|
||||||
|
|
||||||
if (_notification_timer == null) {
|
|
||||||
debug("starting notification timer");
|
|
||||||
_notification_timer = Timer.periodic(
|
|
||||||
Duration(seconds: 5),
|
|
||||||
(timer) {
|
|
||||||
_refreshNotifications();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
void disconnectFromServer() {
|
void disconnectFromServer() {
|
||||||
@ -568,24 +614,25 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
_connected = false;
|
_connected = false;
|
||||||
_connecting = false;
|
_connecting = false;
|
||||||
_token = "";
|
|
||||||
profile = null;
|
profile = null;
|
||||||
|
|
||||||
// Clear received settings
|
// Clear received settings
|
||||||
_globalSettings.clear();
|
_globalSettings.clear();
|
||||||
_userSettings.clear();
|
_userSettings.clear();
|
||||||
|
|
||||||
|
serverInfo.clear();
|
||||||
_connectionStatusChanged();
|
_connectionStatusChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public facing connection function
|
|
||||||
Future<bool> connectToServer() async {
|
/* Public facing connection function.
|
||||||
|
*/
|
||||||
|
Future<bool> connectToServer(UserProfile prf) async {
|
||||||
|
|
||||||
// Ensure server is first disconnected
|
// Ensure server is first disconnected
|
||||||
disconnectFromServer();
|
disconnectFromServer();
|
||||||
|
|
||||||
// Load selected profile
|
profile = prf;
|
||||||
profile = await UserProfileDBManager().getSelectedProfile();
|
|
||||||
|
|
||||||
if (profile == null) {
|
if (profile == null) {
|
||||||
showSnackIcon(
|
showSnackIcon(
|
||||||
@ -596,12 +643,14 @@ class InvenTreeAPI {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_connecting = true;
|
// Cancel notification timer
|
||||||
|
_notification_timer?.cancel();
|
||||||
|
|
||||||
|
_connecting = true;
|
||||||
_connectionStatusChanged();
|
_connectionStatusChanged();
|
||||||
|
|
||||||
_connected = await _connect();
|
// Perform the actual connection routine
|
||||||
|
_connected = await _connectToServer();
|
||||||
_connecting = false;
|
_connecting = false;
|
||||||
|
|
||||||
if (_connected) {
|
if (_connected) {
|
||||||
@ -610,6 +659,15 @@ class InvenTreeAPI {
|
|||||||
icon: FontAwesomeIcons.server,
|
icon: FontAwesomeIcons.server,
|
||||||
success: true,
|
success: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (_notification_timer == null) {
|
||||||
|
debug("starting notification timer");
|
||||||
|
_notification_timer = Timer.periodic(
|
||||||
|
Duration(seconds: 5),
|
||||||
|
(timer) {
|
||||||
|
_refreshNotifications();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_connectionStatusChanged();
|
_connectionStatusChanged();
|
||||||
@ -620,18 +678,13 @@ class InvenTreeAPI {
|
|||||||
/*
|
/*
|
||||||
* Request the user roles (permissions) from the InvenTree server
|
* Request the user roles (permissions) from the InvenTree server
|
||||||
*/
|
*/
|
||||||
Future<bool> getUserRoles() async {
|
Future<bool> _fetchRoles() async {
|
||||||
|
|
||||||
roles.clear();
|
roles.clear();
|
||||||
|
|
||||||
debug("API: Requesting user role data");
|
debug("API: Requesting user role data");
|
||||||
|
|
||||||
// Next we request the permissions assigned to the current user
|
final response = await get(_URL_ROLES, expectedStatusCode: 200);
|
||||||
// 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);
|
|
||||||
|
|
||||||
if (!response.successful()) {
|
if (!response.successful()) {
|
||||||
return false;
|
return false;
|
||||||
@ -645,12 +698,17 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
|
showServerError(
|
||||||
|
apiUrl,
|
||||||
|
L10().serverError,
|
||||||
|
L10().errorUserRoles,
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request plugin information from the server
|
// Request plugin information from the server
|
||||||
Future<bool> getPluginInformation() async {
|
Future<bool> _fetchPlugins() async {
|
||||||
|
|
||||||
_plugins.clear();
|
_plugins.clear();
|
||||||
|
|
||||||
@ -690,7 +748,7 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
if (roles[role] == null) {
|
if (roles[role] == null) {
|
||||||
debug("checkPermission - role '$role' is null!");
|
debug("checkPermission - role '$role' is null!");
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -1045,7 +1103,14 @@ class InvenTreeAPI {
|
|||||||
* @param method is the HTTP method e.g. "POST" / "PATCH" / "GET" etc;
|
* @param method is the HTTP method e.g. "POST" / "PATCH" / "GET" etc;
|
||||||
* @param params is the request parameters
|
* @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);
|
var _url = makeApiUrl(url);
|
||||||
|
|
||||||
@ -1085,11 +1150,16 @@ class InvenTreeAPI {
|
|||||||
try {
|
try {
|
||||||
_request = await client.openUrl(method, _uri).timeout(Duration(seconds: 10));
|
_request = await client.openUrl(method, _uri).timeout(Duration(seconds: 10));
|
||||||
|
|
||||||
// Set headers
|
// Default headers
|
||||||
defaultHeaders().forEach((key, value) {
|
defaultHeaders().forEach((key, value) {
|
||||||
_request?.headers.set(key, value);
|
_request?.headers.set(key, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Custom headers
|
||||||
|
headers.forEach((key, value) {
|
||||||
|
_request?.headers.set(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
return _request;
|
return _request;
|
||||||
} on SocketException catch (error) {
|
} on SocketException catch (error) {
|
||||||
debug("SocketException at ${url}: ${error.toString()}");
|
debug("SocketException at ${url}: ${error.toString()}");
|
||||||
@ -1262,12 +1332,13 @@ class InvenTreeAPI {
|
|||||||
* Perform a HTTP GET request
|
* Perform a HTTP GET request
|
||||||
* Returns a json object (or null if did not complete)
|
* 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(
|
HttpClientRequest? request = await apiRequest(
|
||||||
url,
|
url,
|
||||||
"GET",
|
"GET",
|
||||||
urlParams: params,
|
urlParams: params,
|
||||||
|
headers: headers,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
@ -1334,7 +1405,10 @@ class InvenTreeAPI {
|
|||||||
Map<String, String> defaultHeaders() {
|
Map<String, String> defaultHeaders() {
|
||||||
Map<String, String> headers = {};
|
Map<String, String> headers = {};
|
||||||
|
|
||||||
headers[HttpHeaders.authorizationHeader] = _authorizationHeader();
|
if (hasToken) {
|
||||||
|
headers[HttpHeaders.authorizationHeader] = _authorizationHeader();
|
||||||
|
}
|
||||||
|
|
||||||
headers[HttpHeaders.acceptHeader] = "application/json";
|
headers[HttpHeaders.acceptHeader] = "application/json";
|
||||||
headers[HttpHeaders.contentTypeHeader] = "application/json";
|
headers[HttpHeaders.contentTypeHeader] = "application/json";
|
||||||
headers[HttpHeaders.acceptLanguageHeader] = currentLocale;
|
headers[HttpHeaders.acceptLanguageHeader] = currentLocale;
|
||||||
@ -1342,11 +1416,10 @@ class InvenTreeAPI {
|
|||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Construct a token authorization header
|
||||||
String _authorizationHeader() {
|
String _authorizationHeader() {
|
||||||
if (_token.isNotEmpty) {
|
if (token.isNotEmpty) {
|
||||||
return "Token $_token";
|
return "Token ${token}";
|
||||||
} else if (profile != null) {
|
|
||||||
return "Basic " + base64Encode(utf8.encode("${profile?.username}:${profile?.password}"));
|
|
||||||
} else {
|
} else {
|
||||||
return "";
|
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 {
|
Future<List<InvenTreePOLineItem>> getLineItems() async {
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ Future<Map<String, dynamic>> getDeviceInfo() async {
|
|||||||
"hardware": androidDeviceInfo.hardware,
|
"hardware": androidDeviceInfo.hardware,
|
||||||
"manufacturer": androidDeviceInfo.manufacturer,
|
"manufacturer": androidDeviceInfo.manufacturer,
|
||||||
"product": androidDeviceInfo.product,
|
"product": androidDeviceInfo.product,
|
||||||
"version": androidDeviceInfo.version.release,
|
"systemVersion": androidDeviceInfo.version.release,
|
||||||
"supported32BitAbis": androidDeviceInfo.supported32BitAbis,
|
"supported32BitAbis": androidDeviceInfo.supported32BitAbis,
|
||||||
"supported64BitAbis": androidDeviceInfo.supported64BitAbis,
|
"supported64BitAbis": androidDeviceInfo.supported64BitAbis,
|
||||||
"supportedAbis": androidDeviceInfo.supportedAbis,
|
"supportedAbis": androidDeviceInfo.supportedAbis,
|
||||||
@ -57,7 +57,8 @@ Future<Map<String, dynamic>> getDeviceInfo() async {
|
|||||||
|
|
||||||
|
|
||||||
Map<String, dynamic> getServerInfo() => {
|
Map<String, dynamic> getServerInfo() => {
|
||||||
"version": InvenTreeAPI().version,
|
"version": InvenTreeAPI().serverVersion,
|
||||||
|
"apiVersion": InvenTreeAPI().apiVersion,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -591,6 +591,15 @@
|
|||||||
"locationUpdated": "Stock location updated",
|
"locationUpdated": "Stock location updated",
|
||||||
"@locationUpdated": {},
|
"@locationUpdated": {},
|
||||||
|
|
||||||
|
"login": "Login",
|
||||||
|
"@login": {},
|
||||||
|
|
||||||
|
"loginEnter": "Enter login details",
|
||||||
|
"@loginEnter": {},
|
||||||
|
|
||||||
|
"loginEnterDetails": "Username and password are not stored locally",
|
||||||
|
"@loginEnterDetails": {},
|
||||||
|
|
||||||
"link": "Link",
|
"link": "Link",
|
||||||
"@link": {},
|
"@link": {},
|
||||||
|
|
||||||
@ -795,6 +804,9 @@
|
|||||||
"profileDelete": "Delete Server Profile",
|
"profileDelete": "Delete Server Profile",
|
||||||
"@profileDelete": {},
|
"@profileDelete": {},
|
||||||
|
|
||||||
|
"profileLogout": "Logout Profile",
|
||||||
|
"@profileLogout": {},
|
||||||
|
|
||||||
"profileName": "Profile Name",
|
"profileName": "Profile Name",
|
||||||
"@profileName": {},
|
"@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(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().version),
|
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),
|
leading: FaIcon(FontAwesomeIcons.circleInfo),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -107,13 +115,13 @@ class InvenTreeAboutWidget extends StatelessWidget {
|
|||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().serverInstance),
|
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),
|
leading: FaIcon(FontAwesomeIcons.server),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Display extra tile if the server supports plugins
|
// Display extra tile if the server supports plugins
|
||||||
if (InvenTreeAPI().pluginsEnabled()) {
|
if (InvenTreeAPI().pluginsEnabled) {
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().pluginSupport),
|
title: Text(L10().pluginSupport),
|
||||||
|
@ -1,295 +1,117 @@
|
|||||||
|
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||||
import "package:one_context/one_context.dart";
|
|
||||||
|
|
||||||
import "package:inventree/app_colors.dart";
|
import "package:inventree/app_colors.dart";
|
||||||
import "package:inventree/widget/dialogs.dart";
|
import "package:inventree/user_profile.dart";
|
||||||
import "package:inventree/widget/spinner.dart";
|
|
||||||
import "package:inventree/l10.dart";
|
import "package:inventree/l10.dart";
|
||||||
import "package:inventree/api.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
|
@override
|
||||||
_InvenTreeLoginSettingsState createState() => _InvenTreeLoginSettingsState();
|
_InvenTreeLoginState createState() => _InvenTreeLoginState();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
class _InvenTreeLoginState extends State<InvenTreeLoginWidget> {
|
||||||
|
|
||||||
_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();
|
|
||||||
|
|
||||||
final formKey = GlobalKey<FormState>();
|
final formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
String name = "";
|
|
||||||
String server = "";
|
|
||||||
String username = "";
|
String username = "";
|
||||||
String password = "";
|
String password = "";
|
||||||
|
|
||||||
bool _obscured = true;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(widget.profile == null ? L10().profileAdd : L10().profileEdit),
|
title: Text(L10().login),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: FaIcon(FontAwesomeIcons.floppyDisk),
|
icon: FaIcon(FontAwesomeIcons.arrowRightToBracket, color: COLOR_SUCCESS),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (formKey.currentState!.validate()) {
|
_doLogin(context);
|
||||||
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();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@ -302,79 +124,14 @@ class _ProfileEditState extends State<ProfileEditWidget> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
...before,
|
||||||
TextFormField(
|
TextFormField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: L10().profileName,
|
labelText: L10().username,
|
||||||
labelStyle: TextStyle(fontWeight: FontWeight.bold),
|
labelStyle: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
hintText: L10().enterUsername
|
||||||
),
|
),
|
||||||
initialValue: widget.profile?.name ?? "",
|
initialValue: "",
|
||||||
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 ?? "",
|
|
||||||
keyboardType: TextInputType.text,
|
keyboardType: TextInputType.text,
|
||||||
onSaved: (value) {
|
onSaved: (value) {
|
||||||
username = value?.trim() ?? "";
|
username = value?.trim() ?? "";
|
||||||
@ -388,39 +145,41 @@ class _ProfileEditState extends State<ProfileEditWidget> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: L10().password,
|
labelText: L10().password,
|
||||||
labelStyle: TextStyle(fontWeight: FontWeight.bold),
|
labelStyle: TextStyle(fontWeight: FontWeight.bold),
|
||||||
hintText: L10().enterPassword,
|
hintText: L10().enterPassword,
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: _obscured ? FaIcon(FontAwesomeIcons.eye) : FaIcon(FontAwesomeIcons.solidEyeSlash),
|
icon: _obscured ? FaIcon(FontAwesomeIcons.eye) : FaIcon(FontAwesomeIcons.solidEyeSlash),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_obscured = !_obscured;
|
_obscured = !_obscured;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
initialValue: "",
|
||||||
initialValue: widget.profile?.password ?? "",
|
keyboardType: TextInputType.visiblePassword,
|
||||||
keyboardType: TextInputType.visiblePassword,
|
obscureText: _obscured,
|
||||||
obscureText: _obscured,
|
onSaved: (value) {
|
||||||
onSaved: (value) {
|
password = value?.trim() ?? "";
|
||||||
password = value ?? "";
|
},
|
||||||
},
|
validator: (value) {
|
||||||
validator: (value) {
|
if (value == null || value.trim().isEmpty) {
|
||||||
if (value == null || value.trim().isEmpty) {
|
return L10().passwordEmpty;
|
||||||
return L10().passwordEmpty;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
]
|
...after,
|
||||||
|
],
|
||||||
),
|
),
|
||||||
padding: EdgeInsets.all(16),
|
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/app_settings.dart";
|
||||||
import "package:inventree/settings/barcode_settings.dart";
|
import "package:inventree/settings/barcode_settings.dart";
|
||||||
import "package:inventree/settings/home_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";
|
import "package:inventree/settings/part_settings.dart";
|
||||||
|
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ class _InvenTreeSettingsState extends State<InvenTreeSettingsWidget> {
|
|||||||
subtitle: Text(L10().configureServer),
|
subtitle: Text(L10().configureServer),
|
||||||
leading: FaIcon(FontAwesomeIcons.server, color: COLOR_ACTION),
|
leading: FaIcon(FontAwesomeIcons.server, color: COLOR_ACTION),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeLoginSettingsWidget()));
|
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeSelectServerWidget()));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
@ -10,20 +10,21 @@ class UserProfile {
|
|||||||
this.key,
|
this.key,
|
||||||
this.name = "",
|
this.name = "",
|
||||||
this.server = "",
|
this.server = "",
|
||||||
this.username = "",
|
this.token = "",
|
||||||
this.password = "",
|
|
||||||
this.selected = false,
|
this.selected = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory UserProfile.fromJson(int key, Map<String, dynamic> json, bool isSelected) => UserProfile(
|
factory UserProfile.fromJson(int key, Map<String, dynamic> json, bool isSelected) => UserProfile(
|
||||||
key: key,
|
key: key,
|
||||||
name: json["name"] as String,
|
name: (json["name"] ?? "") as String,
|
||||||
server: json["server"] as String,
|
server: (json["server"] ?? "") as String,
|
||||||
username: json["username"] as String,
|
token: (json["token"] ?? "") as String,
|
||||||
password: json["password"] as String,
|
|
||||||
selected: isSelected,
|
selected: isSelected,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Return true if this profile has a token
|
||||||
|
bool get hasToken => token.isNotEmpty;
|
||||||
|
|
||||||
// ID of the profile
|
// ID of the profile
|
||||||
int? key;
|
int? key;
|
||||||
|
|
||||||
@ -33,11 +34,8 @@ class UserProfile {
|
|||||||
// Base address of the InvenTree server
|
// Base address of the InvenTree server
|
||||||
String server = "";
|
String server = "";
|
||||||
|
|
||||||
// Username
|
// API token
|
||||||
String username = "";
|
String token = "";
|
||||||
|
|
||||||
// Password
|
|
||||||
String password = "";
|
|
||||||
|
|
||||||
bool selected = false;
|
bool selected = false;
|
||||||
|
|
||||||
@ -47,13 +45,12 @@ class UserProfile {
|
|||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
"name": name,
|
"name": name,
|
||||||
"server": server,
|
"server": server,
|
||||||
"username": username,
|
"token": token,
|
||||||
"password": password,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return "<${key}> ${name} : ${server} - ${username}:${password}";
|
return "<${key}> ${name} : ${server}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +85,7 @@ class UserProfileDBManager {
|
|||||||
*/
|
*/
|
||||||
Future<bool> addProfile(UserProfile profile) async {
|
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");
|
debug("addProfile() : Profile missing required values - not adding to database");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -118,7 +115,7 @@ class UserProfileDBManager {
|
|||||||
Future<bool> updateProfile(UserProfile profile) async {
|
Future<bool> updateProfile(UserProfile profile) async {
|
||||||
|
|
||||||
// Prevent invalid profile data from being updated
|
// 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");
|
debug("updateProfile() : Profile missing required values - not updating");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -204,8 +201,6 @@ class UserProfileDBManager {
|
|||||||
UserProfile demoProfile = UserProfile(
|
UserProfile demoProfile = UserProfile(
|
||||||
name: "InvenTree Demo",
|
name: "InvenTree Demo",
|
||||||
server: "https://demo.inventree.org",
|
server: "https://demo.inventree.org",
|
||||||
username: "allaccess",
|
|
||||||
password: "nolimits",
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await addProfile(demoProfile);
|
await addProfile(demoProfile);
|
||||||
@ -217,6 +212,26 @@ class UserProfileDBManager {
|
|||||||
return profileList;
|
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)
|
* 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 {
|
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}";
|
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) {
|
if (details.isNotEmpty) {
|
||||||
extra += "\n";
|
extra += "\n";
|
||||||
extra += details;
|
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
|
* 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/app_colors.dart";
|
||||||
import "package:inventree/preferences.dart";
|
import "package:inventree/preferences.dart";
|
||||||
import "package:inventree/l10.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/user_profile.dart";
|
||||||
|
|
||||||
import "package:inventree/widget/category_display.dart";
|
import "package:inventree/widget/category_display.dart";
|
||||||
@ -119,7 +119,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
|
|||||||
|
|
||||||
void _selectProfile() {
|
void _selectProfile() {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context, MaterialPageRoute(builder: (context) => InvenTreeLoginSettingsWidget())
|
context, MaterialPageRoute(builder: (context) => InvenTreeSelectServerWidget())
|
||||||
).then((context) {
|
).then((context) {
|
||||||
// Once we return
|
// Once we return
|
||||||
_loadProfile();
|
_loadProfile();
|
||||||
@ -147,7 +147,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
|
|||||||
if (!InvenTreeAPI().isConnected() && !InvenTreeAPI().isConnecting()) {
|
if (!InvenTreeAPI().isConnected() && !InvenTreeAPI().isConnecting()) {
|
||||||
|
|
||||||
// Attempt server connection
|
// Attempt server connection
|
||||||
InvenTreeAPI().connectToServer().then((result) {
|
InvenTreeAPI().connectToServer(_profile!).then((result) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
@ -17,37 +17,11 @@ void main() {
|
|||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
|
|
||||||
if (! await UserProfileDBManager().profileNameExists("Test Profile")) {
|
await setupServerProfile(select: true);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the profile is selected
|
// Ensure the profile is selected
|
||||||
assert(! await UserProfileDBManager().selectProfileByName("Missing Profile"));
|
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();
|
var api = InvenTreeAPI();
|
||||||
|
|
||||||
// Incorrect server address
|
// Incorrect server address
|
||||||
var profile = await UserProfileDBManager().getSelectedProfile();
|
var profile = await setupServerProfile();
|
||||||
|
|
||||||
assert(profile != null);
|
profile.server = "http://localhost:5555";
|
||||||
|
|
||||||
if (profile != null) {
|
bool result = await api.connectToServer(profile);
|
||||||
profile.server = "http://localhost:5555";
|
assert(!result);
|
||||||
await UserProfileDBManager().updateProfile(profile);
|
|
||||||
|
|
||||||
bool result = await api.connectToServer();
|
debugContains("SocketException at");
|
||||||
assert(!result);
|
|
||||||
|
|
||||||
debugContains("SocketException at");
|
// Test incorrect login details
|
||||||
|
profile.server = testServerAddress;
|
||||||
|
|
||||||
// Test incorrect login details
|
final response = await api.fetchToken(profile, "baduser", "badpassword");
|
||||||
profile.server = "http://localhost:12345";
|
assert(!response.successful());
|
||||||
profile.username = "invalidusername";
|
|
||||||
|
|
||||||
await UserProfileDBManager().updateProfile(profile);
|
debugContains("Token request failed");
|
||||||
|
|
||||||
await api.connectToServer();
|
assert(!api.checkConnection());
|
||||||
assert(!result);
|
|
||||||
|
|
||||||
debugContains("Token request failed");
|
debugContains("Token request failed: STATUS 401");
|
||||||
|
debugContains("showSnackIcon: 'Not Connected'");
|
||||||
|
|
||||||
assert(!api.checkConnection());
|
});
|
||||||
|
|
||||||
debugContains("Token request failed: STATUS 401");
|
test("Bad Token", () async {
|
||||||
debugContains("showSnackIcon: 'Not Connected'");
|
// Test that login fails with a bad token
|
||||||
|
var profile = await setupServerProfile();
|
||||||
|
|
||||||
} else {
|
profile.token = "bad-token";
|
||||||
assert(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
bool result = await InvenTreeAPI().connectToServer(profile);
|
||||||
|
assert(!result);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Login Success", () async {
|
test("Login Success", () async {
|
||||||
// Test that we can login to the server successfully
|
// Test that we can login to the server successfully
|
||||||
var api = InvenTreeAPI();
|
var api = InvenTreeAPI();
|
||||||
|
|
||||||
// Attempt to connect
|
final profile = await setupServerProfile(select: true, fetchToken: true);
|
||||||
final bool result = await api.connectToServer();
|
assert(profile.hasToken);
|
||||||
|
|
||||||
|
// Now, connect to the server
|
||||||
|
bool result = await api.connectToServer(profile);
|
||||||
|
|
||||||
// Check expected values
|
// Check expected values
|
||||||
assert(result);
|
assert(result);
|
||||||
assert(api.hasToken);
|
assert(api.hasToken);
|
||||||
expect(api.baseUrl, equals("http://localhost:12345/"));
|
|
||||||
|
|
||||||
|
expect(api.baseUrl, equals(testServerAddress));
|
||||||
|
|
||||||
|
assert(api.hasToken);
|
||||||
assert(api.isConnected());
|
assert(api.isConnected());
|
||||||
assert(!api.isConnecting());
|
assert(!api.isConnecting());
|
||||||
assert(api.checkConnection());
|
assert(api.checkConnection());
|
||||||
@ -127,7 +105,8 @@ void main() {
|
|||||||
// Test server version information
|
// Test server version information
|
||||||
var api = InvenTreeAPI();
|
var api = InvenTreeAPI();
|
||||||
|
|
||||||
assert(await api.connectToServer());
|
final profile = await setupServerProfile(fetchToken: true);
|
||||||
|
assert(await api.connectToServer(profile));
|
||||||
|
|
||||||
// Check supported functions
|
// Check supported functions
|
||||||
assert(api.apiVersion >= 50);
|
assert(api.apiVersion >= 50);
|
||||||
@ -135,12 +114,15 @@ void main() {
|
|||||||
assert(api.supportsNotifications);
|
assert(api.supportsNotifications);
|
||||||
assert(api.supportsPoReceive);
|
assert(api.supportsPoReceive);
|
||||||
|
|
||||||
// Ensure we can request (and receive) user roles
|
assert(api.serverInstance.isNotEmpty);
|
||||||
assert(await api.getUserRoles());
|
assert(api.serverVersion.isNotEmpty);
|
||||||
|
|
||||||
|
// Ensure we can have user role data
|
||||||
|
assert(api.roles.isNotEmpty);
|
||||||
|
|
||||||
// Check available permissions
|
// Check available permissions
|
||||||
assert(api.checkPermission("part", "change"));
|
assert(api.checkPermission("part", "change"));
|
||||||
assert(api.checkPermission("stocklocation", "delete"));
|
assert(api.checkPermission("stock_location", "delete"));
|
||||||
assert(!api.checkPermission("part", "weirdpermission"));
|
assert(!api.checkPermission("part", "weirdpermission"));
|
||||||
assert(api.checkPermission("blah", "bloo"));
|
assert(api.checkPermission("blah", "bloo"));
|
||||||
|
|
||||||
|
@ -10,7 +10,6 @@ import "package:flutter_test/flutter_test.dart";
|
|||||||
import "package:inventree/api.dart";
|
import "package:inventree/api.dart";
|
||||||
import "package:inventree/barcode/barcode.dart";
|
import "package:inventree/barcode/barcode.dart";
|
||||||
import "package:inventree/helpers.dart";
|
import "package:inventree/helpers.dart";
|
||||||
import "package:inventree/user_profile.dart";
|
|
||||||
|
|
||||||
import "package:inventree/inventree/part.dart";
|
import "package:inventree/inventree/part.dart";
|
||||||
import "package:inventree/inventree/stock.dart";
|
import "package:inventree/inventree/stock.dart";
|
||||||
@ -23,26 +22,7 @@ void main() {
|
|||||||
|
|
||||||
// Connect to the server
|
// Connect to the server
|
||||||
setUpAll(() async {
|
setUpAll(() async {
|
||||||
final prf = await UserProfileDBManager().getProfileByName("Test Profile");
|
await connectToTestServer();
|
||||||
|
|
||||||
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());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
@ -91,8 +71,8 @@ void main() {
|
|||||||
test("Scan Into Location", () async {
|
test("Scan Into Location", () async {
|
||||||
|
|
||||||
final item = await InvenTreeStockItem().get(1) as InvenTreeStockItem?;
|
final item = await InvenTreeStockItem().get(1) as InvenTreeStockItem?;
|
||||||
|
|
||||||
assert(item != null);
|
assert(item != null);
|
||||||
|
|
||||||
assert(item!.pk == 1);
|
assert(item!.pk == 1);
|
||||||
|
|
||||||
var handler = StockItemScanIntoLocationHandler(item!);
|
var handler = StockItemScanIntoLocationHandler(item!);
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
import "package:test/test.dart";
|
import "package:test/test.dart";
|
||||||
|
|
||||||
import "package:inventree/api.dart";
|
import "package:inventree/api.dart";
|
||||||
import "package:inventree/user_profile.dart";
|
|
||||||
import "package:inventree/inventree/model.dart";
|
import "package:inventree/inventree/model.dart";
|
||||||
import "package:inventree/inventree/part.dart";
|
import "package:inventree/inventree/part.dart";
|
||||||
|
|
||||||
@ -16,16 +15,7 @@ void main() {
|
|||||||
setupTestEnv();
|
setupTestEnv();
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
await UserProfileDBManager().addProfile(UserProfile(
|
await connectToTestServer();
|
||||||
name: "Test Profile",
|
|
||||||
server: "http://localhost:12345",
|
|
||||||
username: "testuser",
|
|
||||||
password: "testpassword",
|
|
||||||
selected: true,
|
|
||||||
));
|
|
||||||
|
|
||||||
assert(await UserProfileDBManager().selectProfileByName("Test Profile"));
|
|
||||||
assert(await InvenTreeAPI().connectToServer());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
group("Category Tests:", () {
|
group("Category Tests:", () {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
|
||||||
import "package:flutter/services.dart";
|
import "package:flutter/services.dart";
|
||||||
import "package:flutter_test/flutter_test.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
|
// This is the same as the following issue except it keeps the http client
|
||||||
// TestWidgetsFlutterBinding.ensureInitialized();
|
// TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
@ -19,4 +21,78 @@ void setupTestEnv() {
|
|||||||
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
|
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
|
||||||
return ".";
|
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!
|
// Now, create one!
|
||||||
bool result = await UserProfileDBManager().addProfile(UserProfile(
|
bool result = await UserProfileDBManager().addProfile(UserProfile(
|
||||||
name: "Test Profile",
|
name: testServerName,
|
||||||
username: "testuser",
|
server: testServerAddress,
|
||||||
password: "testpassword""",
|
|
||||||
server: "http://localhost:12345",
|
|
||||||
selected: true,
|
selected: true,
|
||||||
));
|
));
|
||||||
|
|
||||||
@ -62,20 +60,15 @@ void main() {
|
|||||||
test("Add Invalid Profiles", () async {
|
test("Add Invalid Profiles", () async {
|
||||||
// Add a profile with missing data
|
// Add a profile with missing data
|
||||||
bool result = await UserProfileDBManager().addProfile(
|
bool result = await UserProfileDBManager().addProfile(
|
||||||
UserProfile(
|
UserProfile()
|
||||||
username: "what",
|
|
||||||
password: "why",
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result, equals(false));
|
expect(result, equals(false));
|
||||||
|
|
||||||
// Add a profile with a name that already exists
|
// Add a profile with a new name
|
||||||
result = await UserProfileDBManager().addProfile(
|
result = await UserProfileDBManager().addProfile(
|
||||||
UserProfile(
|
UserProfile(
|
||||||
name: "Test Profile",
|
name: "Another Test Profile",
|
||||||
username: "xyz",
|
|
||||||
password: "hunter42",
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -84,14 +77,14 @@ void main() {
|
|||||||
// Check that the number of protocols available is still the same
|
// Check that the number of protocols available is still the same
|
||||||
var profiles = await UserProfileDBManager().getAllProfiles();
|
var profiles = await UserProfileDBManager().getAllProfiles();
|
||||||
|
|
||||||
expect(profiles.length, equals(1));
|
expect(profiles.length, equals(2));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Profile Name Check", () async {
|
test("Profile Name Check", () async {
|
||||||
bool result = await UserProfileDBManager().profileNameExists("doesnotexist");
|
bool result = await UserProfileDBManager().profileNameExists("doesnotexist");
|
||||||
expect(result, equals(false));
|
expect(result, equals(false));
|
||||||
|
|
||||||
result = await UserProfileDBManager().profileNameExists("Test Profile");
|
result = await UserProfileDBManager().profileNameExists("Test Server");
|
||||||
expect(result, equals(true));
|
expect(result, equals(true));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -104,23 +97,16 @@ void main() {
|
|||||||
if (prf != null) {
|
if (prf != null) {
|
||||||
UserProfile p = prf;
|
UserProfile p = prf;
|
||||||
|
|
||||||
expect(p.name, equals("Test Profile"));
|
expect(p.name, equals(testServerName));
|
||||||
expect(p.username, equals("testuser"));
|
expect(p.server, equals(testServerAddress));
|
||||||
expect(p.password, equals("testpassword"));
|
|
||||||
expect(p.server, equals("http://localhost:12345"));
|
|
||||||
|
|
||||||
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
|
// Test that we can update the profile
|
||||||
p.name = "different name";
|
p.name = "different name";
|
||||||
|
|
||||||
bool result = await UserProfileDBManager().updateProfile(p);
|
bool result = await UserProfileDBManager().updateProfile(p);
|
||||||
expect(result, equals(true));
|
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