mirror of
https://github.com/inventree/inventree-app.git
synced 2026-07-21 19:55:55 +00:00
* Updated dashboard - Display "outstanding" badges - Display "overdue" badges - Pull-to-refresh * Remove dead code * Refactor app colors * Add "pending shipments" icon * Remove custom spinner * Refactor error dialog * Updated redirect after login * Refactor login/account pages - Better UX and confirmation messages * Refactor API error messages * Improve token management - Secure storage - Handle session expiry - Per-profile HTTPS certificate checks * Improved error messages
192 lines
5.5 KiB
Dart
192 lines
5.5 KiB
Dart
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/widget/dialogs.dart";
|
|
import "package:inventree/widget/progress.dart";
|
|
|
|
class InvenTreeLoginWidget extends StatefulWidget {
|
|
const InvenTreeLoginWidget(this.profile) : super();
|
|
|
|
final UserProfile profile;
|
|
|
|
@override
|
|
_InvenTreeLoginState createState() => _InvenTreeLoginState();
|
|
}
|
|
|
|
class _InvenTreeLoginState extends State<InvenTreeLoginWidget> {
|
|
final formKey = GlobalKey<FormState>();
|
|
|
|
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();
|
|
|
|
// Attempt login - suppress the generic error dialog, since this screen
|
|
// shows its own contextual inline error message below
|
|
final response = await InvenTreeAPI().fetchToken(
|
|
widget.profile,
|
|
username,
|
|
password,
|
|
showDialog: false,
|
|
);
|
|
|
|
if (response.successful()) {
|
|
// A token was issued - immediately connect using it, then return
|
|
// directly to the home screen (rather than leaving the user on the
|
|
// server-selector screen to navigate back manually)
|
|
await InvenTreeAPI().connectToServer(widget.profile);
|
|
|
|
hideLoadingOverlay();
|
|
|
|
if (context.mounted) {
|
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
|
}
|
|
} else {
|
|
hideLoadingOverlay();
|
|
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: Icon(TablerIcons.user_check),
|
|
),
|
|
ListTile(
|
|
title: Text(L10().server),
|
|
subtitle: Text(widget.profile.server),
|
|
leading: Icon(TablerIcons.server),
|
|
),
|
|
Divider(),
|
|
];
|
|
|
|
List<Widget> after = [];
|
|
|
|
if (error.isNotEmpty) {
|
|
after.add(Divider());
|
|
after.add(
|
|
ListTile(
|
|
leading: Icon(TablerIcons.exclamation_circle, 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(L10().login),
|
|
actions: [
|
|
IconButton(
|
|
icon: Icon(TablerIcons.transition_right, color: COLOR_SUCCESS),
|
|
onPressed: () async {
|
|
_doLogin(context);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
body: Form(
|
|
key: formKey,
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.max,
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
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;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
initialValue: "",
|
|
keyboardType: TextInputType.visiblePassword,
|
|
obscureText: _obscured,
|
|
onSaved: (value) {
|
|
password = value?.trim() ?? "";
|
|
},
|
|
validator: (value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return L10().passwordEmpty;
|
|
}
|
|
|
|
return null;
|
|
},
|
|
),
|
|
...after,
|
|
],
|
|
),
|
|
padding: EdgeInsets.all(16),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|