This commit is contained in:
Raffael Wolf 2026-01-31 20:45:53 +00:00 committed by GitHub
commit 651cd73d5a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 197 additions and 69 deletions

View file

@ -2,32 +2,31 @@ import "dart:async";
import "dart:convert"; import "dart:convert";
import "dart:io"; import "dart:io";
import "package:cached_network_image/cached_network_image.dart";
import "package:flutter/foundation.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/http.dart" as http;
import "package:http/io_client.dart"; import "package:http/io_client.dart";
import "package:intl/intl.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/api_form.dart";
import "package:inventree/app_colors.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/helpers.dart";
import "package:inventree/inventree/model.dart"; import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/notification.dart"; import "package:inventree/inventree/notification.dart";
import "package:inventree/inventree/status_codes.dart";
import "package:inventree/inventree/sentry.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/user_profile.dart";
import "package:inventree/widget/dialogs.dart"; import "package:inventree/widget/dialogs.dart";
import "package:inventree/widget/progress.dart";
import "package:inventree/widget/snacks.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 * Class representing an API response from the server
@ -163,6 +162,7 @@ class InvenTreeFileService extends FileService {
* *
* InvenTree implements token-based authentication, which is * InvenTree implements token-based authentication, which is
* initialised using a username:password combination. * 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<APIResponse> 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, * Fetch a token from the server,
* with a temporary authentication header * with a temporary authentication header

View file

@ -417,6 +417,9 @@
"enterPassword": "Enter password", "enterPassword": "Enter password",
"@enterPassword": {}, "@enterPassword": {},
"enterToken": "Enter API token",
"@enterToken": {},
"enterUsername": "Enter username", "enterUsername": "Enter username",
"@enterUsername": {}, "@enterUsername": {},
@ -664,6 +667,9 @@
"invalidSupplierPart": "Invalid Supplier Part", "invalidSupplierPart": "Invalid Supplier Part",
"@invalidSupplierPart": {}, "@invalidSupplierPart": {},
"invalidToken": "Invalid Token",
"@invalidToken": {},
"invalidUsernamePassword": "Invalid username / password combination", "invalidUsernamePassword": "Invalid username / password combination",
"@invalidUsernamePassword": {}, "@invalidUsernamePassword": {},
@ -769,6 +775,9 @@
"login": "Login", "login": "Login",
"@login": {}, "@login": {},
"loginMethod": "Login method",
"@loginMethod": {},
"loginEnter": "Enter login details", "loginEnter": "Enter login details",
"@loginEnter": {}, "@loginEnter": {},
@ -1643,6 +1652,12 @@
"toggleTorch": "Toggle Torch", "toggleTorch": "Toggle Torch",
"@toggleTorch": {}, "@toggleTorch": {},
"token": "Token",
"@token": {},
"tokenEmpty": "Token cannot be empty",
"@tokenEmpty": {},
"tokenError": "Token Error", "tokenError": "Token Error",
"@tokenError": {}, "@tokenError": {},
@ -1719,6 +1734,9 @@
"username": "Username", "username": "Username",
"@username": {}, "@username": {},
"usernameAndPassword": "Username & Password",
"@usernameAndPassword": {},
"usernameEmpty": "Username cannot be empty", "usernameEmpty": "Username cannot be empty",
"@usernameEmpty": {}, "@usernameEmpty": {},

View file

@ -1,13 +1,14 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.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/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/dialogs.dart";
import "package:inventree/widget/progress.dart"; import "package:inventree/widget/progress.dart";
enum LoginMethod { credentials, token }
class InvenTreeLoginWidget extends StatefulWidget { class InvenTreeLoginWidget extends StatefulWidget {
const InvenTreeLoginWidget(this.profile) : super(); const InvenTreeLoginWidget(this.profile) : super();
@ -22,9 +23,12 @@ class _InvenTreeLoginState extends State<InvenTreeLoginWidget> {
String username = ""; String username = "";
String password = ""; String password = "";
String token = "";
bool _obscured = true; bool _obscured = true;
LoginMethod _method = LoginMethod.credentials;
String error = ""; String error = "";
// Attempt login // Attempt login
@ -44,12 +48,19 @@ class _InvenTreeLoginState extends State<InvenTreeLoginWidget> {
showLoadingOverlay(); showLoadingOverlay();
// Attempt login final APIResponse response;
final response = await InvenTreeAPI().fetchToken(
widget.profile, if (_method == LoginMethod.credentials) {
username, // Attempt login
password, response = await InvenTreeAPI().fetchToken(
); widget.profile,
username,
password,
);
} else {
// Check token validity
response = await InvenTreeAPI().checkToken(widget.profile, token);
}
hideLoadingOverlay(); hideLoadingOverlay();
@ -123,55 +134,120 @@ class _InvenTreeLoginState extends State<InvenTreeLoginWidget> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
...before, ...before,
TextFormField( // Dropdown to select login method
decoration: InputDecoration( Padding(
labelText: L10().username, padding: EdgeInsets.symmetric(vertical: 8),
labelStyle: TextStyle(fontWeight: FontWeight.bold), child: DropdownButtonFormField<LoginMethod>(
hintText: L10().enterUsername, value: _method,
), decoration: InputDecoration(
initialValue: "", labelText: L10().loginMethod,
keyboardType: TextInputType.text, labelStyle: TextStyle(fontWeight: FontWeight.bold),
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;
});
},
), ),
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, ...after,
], ],
), ),