mirror of
https://github.com/inventree/inventree-app.git
synced 2025-06-13 02:35:27 +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:
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(() {});
|
||||
}
|
||||
|
Reference in New Issue
Block a user