From e08139671d37e562fd0a7a2ee88eaf48233ee262 Mon Sep 17 00:00:00 2001 From: RaffaelW <146560011+RaffaelW@users.noreply.github.com> Date: Wed, 28 Jan 2026 23:15:20 +0100 Subject: [PATCH] Implement token-based login Added drop-down to switch between username & password login or token login so login if using OAuth is possible. --- lib/api.dart | 35 ++++++++ lib/l10n/app_en.arb | 18 ++++ lib/settings/login.dart | 181 ++++++++++++++++++++++++++++------------ 3 files changed, 182 insertions(+), 52 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index 61a63798..b15c555e 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -163,6 +163,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 +541,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 4f6560f3..fde7eff9 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -418,6 +418,9 @@ "enterPassword": "Enter password", "@enterPassword": {}, + "enterToken": "Enter API token", + "@enterToken": {}, + "enterUsername": "Enter username", "@enterUsername": {}, @@ -665,6 +668,9 @@ "invalidSupplierPart": "Invalid Supplier Part", "@invalidSupplierPart": {}, + "invalidToken": "Invalid token", + "@invalidToken": {}, + "invalidUsernamePassword": "Invalid username / password combination", "@invalidUsernamePassword": {}, @@ -770,6 +776,9 @@ "login": "Login", "@login": {}, + "loginMethod": "Login method", + "@loginMethod": {}, + "loginEnter": "Enter login details", "@loginEnter": {}, @@ -1644,6 +1653,12 @@ "toggleTorch": "Toggle Torch", "@toggleTorch": {}, + "token": "Token", + "@token": {}, + + "tokenEmpty": "Token cannot be empty", + "@tokenEmpty": {}, + "tokenError": "Token Error", "@tokenError": {}, @@ -1720,6 +1735,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 2e04d498..95d11f1d 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -8,6 +8,8 @@ import "package:inventree/api.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 +24,12 @@ class _InvenTreeLoginState extends State { String username = ""; String password = ""; + String token = ""; bool _obscured = true; + LoginMethod _method = LoginMethod.credentials; + String error = ""; // Attempt login @@ -44,12 +49,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 +135,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, ], ),