diff --git a/lib/api.dart b/lib/api.dart index 61a6379..61a9209 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -2,32 +2,31 @@ import "dart:async"; import "dart:convert"; import "dart:io"; +import "package:cached_network_image/cached_network_image.dart"; import "package:flutter/foundation.dart"; +import "package:flutter/material.dart"; +import "package:flutter_cache_manager/flutter_cache_manager.dart"; +import "package:flutter_tabler_icons/flutter_tabler_icons.dart"; import "package:http/http.dart" as http; import "package:http/io_client.dart"; import "package:intl/intl.dart"; -import "package:inventree/main.dart"; -import "package:inventree/widget/progress.dart"; -import "package:one_context/one_context.dart"; -import "package:open_filex/open_filex.dart"; -import "package:cached_network_image/cached_network_image.dart"; -import "package:flutter/material.dart"; -import "package:flutter_tabler_icons/flutter_tabler_icons.dart"; -import "package:flutter_cache_manager/flutter_cache_manager.dart"; -import "package:path_provider/path_provider.dart"; - import "package:inventree/api_form.dart"; import "package:inventree/app_colors.dart"; -import "package:inventree/preferences.dart"; -import "package:inventree/l10.dart"; import "package:inventree/helpers.dart"; import "package:inventree/inventree/model.dart"; import "package:inventree/inventree/notification.dart"; -import "package:inventree/inventree/status_codes.dart"; import "package:inventree/inventree/sentry.dart"; +import "package:inventree/inventree/status_codes.dart"; +import "package:inventree/l10.dart"; +import "package:inventree/main.dart"; +import "package:inventree/preferences.dart"; import "package:inventree/user_profile.dart"; import "package:inventree/widget/dialogs.dart"; +import "package:inventree/widget/progress.dart"; import "package:inventree/widget/snacks.dart"; +import "package:one_context/one_context.dart"; +import "package:open_filex/open_filex.dart"; +import "package:path_provider/path_provider.dart"; /* * Class representing an API response from the server @@ -163,6 +162,7 @@ class InvenTreeFileService extends FileService { * * InvenTree implements token-based authentication, which is * initialised using a username:password combination. + * Alternatively, an existing token provided by the user can be used for authentication. */ /* @@ -540,6 +540,40 @@ class InvenTreeAPI { } } + Future checkToken(UserProfile userProfile, String token) async { + debug("Checking token @ ${_URL_ME}"); + + userProfile.token = token; + profile = userProfile; + + final response = await get(_URL_ME); + + if (!response.successful()) { + switch (response.statusCode) { + case 401: + case 403: + showServerError( + apiUrl, + L10().serverAuthenticationError, + L10().invalidToken, + ); + default: + showStatusCodeError(apiUrl, response.statusCode); + } + + debug("Request failed: STATUS ${response.statusCode}"); + + // reset token + userProfile.token = ""; + profile = userProfile; + } else { + // save token + await UserProfileDBManager().updateProfile(userProfile); + } + + return response; + } + /* * Fetch a token from the server, * with a temporary authentication header diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 4f6560f..0dcdec9 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -417,6 +417,9 @@ "enterPassword": "Enter password", "@enterPassword": {}, + + "enterToken": "Enter API token", + "@enterToken": {}, "enterUsername": "Enter username", "@enterUsername": {}, @@ -664,6 +667,9 @@ "invalidSupplierPart": "Invalid Supplier Part", "@invalidSupplierPart": {}, + + "invalidToken": "Invalid Token", + "@invalidToken": {}, "invalidUsernamePassword": "Invalid username / password combination", "@invalidUsernamePassword": {}, @@ -769,6 +775,9 @@ "login": "Login", "@login": {}, + + "loginMethod": "Login method", + "@loginMethod": {}, "loginEnter": "Enter login details", "@loginEnter": {}, @@ -1643,6 +1652,12 @@ "toggleTorch": "Toggle Torch", "@toggleTorch": {}, + + "token": "Token", + "@token": {}, + + "tokenEmpty": "Token cannot be empty", + "@tokenEmpty": {}, "tokenError": "Token Error", "@tokenError": {}, @@ -1719,6 +1734,9 @@ "username": "Username", "@username": {}, + + "usernameAndPassword": "Username & Password", + "@usernameAndPassword": {}, "usernameEmpty": "Username cannot be empty", "@usernameEmpty": {}, diff --git a/lib/settings/login.dart b/lib/settings/login.dart index 2e04d49..6fc0b63 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -1,13 +1,14 @@ import "package:flutter/material.dart"; import "package:flutter_tabler_icons/flutter_tabler_icons.dart"; - -import "package:inventree/app_colors.dart"; -import "package:inventree/user_profile.dart"; -import "package:inventree/l10.dart"; import "package:inventree/api.dart"; +import "package:inventree/app_colors.dart"; +import "package:inventree/l10.dart"; +import "package:inventree/user_profile.dart"; import "package:inventree/widget/dialogs.dart"; import "package:inventree/widget/progress.dart"; +enum LoginMethod { credentials, token } + class InvenTreeLoginWidget extends StatefulWidget { const InvenTreeLoginWidget(this.profile) : super(); @@ -22,9 +23,12 @@ class _InvenTreeLoginState extends State { String username = ""; String password = ""; + String token = ""; bool _obscured = true; + LoginMethod _method = LoginMethod.credentials; + String error = ""; // Attempt login @@ -44,12 +48,19 @@ class _InvenTreeLoginState extends State { showLoadingOverlay(); - // Attempt login - final response = await InvenTreeAPI().fetchToken( - widget.profile, - username, - password, - ); + final APIResponse response; + + if (_method == LoginMethod.credentials) { + // Attempt login + response = await InvenTreeAPI().fetchToken( + widget.profile, + username, + password, + ); + } else { + // Check token validity + response = await InvenTreeAPI().checkToken(widget.profile, token); + } hideLoadingOverlay(); @@ -123,55 +134,120 @@ class _InvenTreeLoginState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ ...before, - TextFormField( - decoration: InputDecoration( - labelText: L10().username, - labelStyle: TextStyle(fontWeight: FontWeight.bold), - hintText: L10().enterUsername, - ), - initialValue: "", - keyboardType: TextInputType.text, - onSaved: (value) { - username = value?.trim() ?? ""; - }, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return L10().usernameEmpty; - } - - return null; - }, - ), - TextFormField( - decoration: InputDecoration( - labelText: L10().password, - labelStyle: TextStyle(fontWeight: FontWeight.bold), - hintText: L10().enterPassword, - suffixIcon: IconButton( - icon: _obscured - ? Icon(TablerIcons.eye) - : Icon(TablerIcons.eye_off), - onPressed: () { - setState(() { - _obscured = !_obscured; - }); - }, + // Dropdown to select login method + Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: DropdownButtonFormField( + value: _method, + decoration: InputDecoration( + labelText: L10().loginMethod, + labelStyle: TextStyle(fontWeight: FontWeight.bold), ), + items: [ + DropdownMenuItem( + child: Text(L10().usernameAndPassword), + value: LoginMethod.credentials, + ), + DropdownMenuItem( + child: Text(L10().token), + value: LoginMethod.token, + ), + ], + onChanged: (val) { + setState(() { + _method = val ?? LoginMethod.credentials; + error = ""; + }); + }, ), - initialValue: "", - keyboardType: TextInputType.visiblePassword, - obscureText: _obscured, - onSaved: (value) { - password = value?.trim() ?? ""; - }, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return L10().passwordEmpty; - } - - return null; - }, ), + + // Show fields depending on selected login method + if (_method == LoginMethod.credentials) ...[ + TextFormField( + key: ValueKey("input-username"), + decoration: InputDecoration( + labelText: L10().username, + labelStyle: TextStyle(fontWeight: FontWeight.bold), + hintText: L10().enterUsername, + ), + initialValue: "", + keyboardType: TextInputType.text, + onSaved: (value) { + username = value?.trim() ?? ""; + }, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return L10().usernameEmpty; + } + + return null; + }, + ), + TextFormField( + key: ValueKey("input-password"), + decoration: InputDecoration( + labelText: L10().password, + labelStyle: TextStyle(fontWeight: FontWeight.bold), + hintText: L10().enterPassword, + suffixIcon: IconButton( + icon: _obscured + ? Icon(TablerIcons.eye) + : Icon(TablerIcons.eye_off), + onPressed: () { + setState(() { + _obscured = !_obscured; + }); + }, + ), + ), + initialValue: "", + keyboardType: TextInputType.visiblePassword, + obscureText: _obscured, + onSaved: (value) { + password = value?.trim() ?? ""; + }, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return L10().passwordEmpty; + } + + return null; + }, + ), + ] else ...[ + TextFormField( + key: ValueKey("input-token"), + decoration: InputDecoration( + labelText: L10().token, + labelStyle: TextStyle(fontWeight: FontWeight.bold), + hintText: L10().enterToken, + suffixIcon: IconButton( + icon: _obscured + ? Icon(TablerIcons.eye) + : Icon(TablerIcons.eye_off), + onPressed: () { + setState(() { + _obscured = !_obscured; + }); + }, + ), + ), + initialValue: "", + keyboardType: TextInputType.visiblePassword, + obscureText: _obscured, + onSaved: (value) { + token = value?.trim() ?? ""; + }, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return L10().tokenEmpty; + } + + return null; + }, + ), + ], ...after, ], ),