--- /dev/null
+import 'package:built_collection/built_collection.dart';
+import 'package:built_value/built_value.dart';
+
+extension BuiltValueListBuilderExtension<V extends Built<V, B>,
+ B extends Builder<V, B>> on ListBuilder<Built<V, B>> {
+ void rebuildWhere(bool Function(V) test, void Function(B) updates) {
+ for (var i = 0; i != this.length; ++i) {
+ if (test(this[i])) this[i] = this[i].rebuild(updates);
+ }
+ }
+}
import 'package:proxmox_login_manager/proxmox_general_settings_model.dart';
import 'package:proxmox_login_manager/proxmox_login_model.dart';
import 'package:proxmox_login_manager/proxmox_tfa_form.dart';
+import 'package:proxmox_login_manager/extension.dart';
class ProxmoxProgressModel {
bool inProgress;
bool _obscure = true;
FocusNode passwordFocusNode;
- @override
- void initState() {
- super.initState();
-
- if (widget.usernameController.text.isNotEmpty) {
- passwordFocusNode = FocusNode();
- passwordFocusNode.requestFocus();
- }
- }
-
@override
Widget build(BuildContext context) {
if (widget.accessDomains == null) {
class ProxmoxLoginPage extends StatefulWidget {
final ProxmoxLoginModel userModel;
final bool isCreate;
+ final String ticket;
const ProxmoxLoginPage({
Key key,
this.userModel,
this.isCreate,
+ this.ticket = '',
}) : super(key: key);
@override
_ProxmoxLoginPageState createState() => _ProxmoxLoginPageState();
final _formKey = GlobalKey<FormState>();
ProxmoxProgressModel _progressModel;
bool _submittButtonEnabled = true;
+
@override
void initState() {
super.initState();
'${userModel.origin?.host}:${userModel.origin?.port}';
_accessDomains = _getAccessDomains();
_usernameController.text = userModel.username;
+ if (widget.ticket.isNotEmpty) {
+ _onLoginButtonPressed(ticket: widget.ticket, mRealm: userModel.realm);
+ }
}
}
);
}
- Future<void> _onLoginButtonPressed() async {
+ Future<void> _onLoginButtonPressed(
+ {String ticket = '', String mRealm}) async {
setState(() {
_progressModel
..inProgress = true
//cleaned form fields
final origin = Uri.https(_originController.text.trim(), '');
final username = _usernameController.text.trim();
- final password = _passwordController.text.trim();
- final realm = _selectedDomain.realm;
+ final password =
+ ticket.isNotEmpty ? ticket : _passwordController.text.trim();
+ final realm = _selectedDomain?.realm ?? mRealm;
var client = await proxclient.authenticate(
'$username@$realm', password, origin, settings.sslValidation);
));
}
+ final status = await client.getClusterStatus();
+ final hostname =
+ status.singleWhere((element) => element.local ?? false).name;
var loginStorage = await ProxmoxLoginStorage.fromLocalStorage();
if (widget.isCreate) {
..username = username
..realm = realm
..productType = ProxmoxProductType.pve
- ..ticket = client.credentials.ticket);
+ ..ticket = client.credentials.ticket
+ ..hostname = hostname);
loginStorage = loginStorage.rebuild((b) => b..logins.add(newLogin));
} else {
loginStorage = loginStorage.rebuild((b) => b
- ..logins.remove(widget.userModel)
- ..logins.add(widget.userModel
- .rebuild((b) => b..ticket = client.credentials.ticket)));
+ ..logins.rebuildWhere(
+ (m) => m == widget.userModel,
+ (b) => b
+ ..ticket = client.credentials.ticket
+ ..hostname = hostname));
}
await loginStorage.saveToDisk();
/// The username with the corresponding realm e.g. root@pam
String get fullUsername => '$username@$realm';
+ bool get activeSession => ticket != null && ticket.isNotEmpty;
+
+ @nullable
+ String get hostname;
+
+ String get fullHostname {
+ if (origin.host == hostname) {
+ return hostname;
+ }
+ if (hostname == null || hostname.isEmpty) {
+ return origin.host;
+ }
+
+ return '${origin.host} - $hostname';
+ }
+
ProxmoxLoginModel._();
factory ProxmoxLoginModel([void Function(ProxmoxLoginModelBuilder) updates]) =
import 'package:proxmox_login_manager/proxmox_login_model.dart';
import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart'
as proxclient;
+import 'package:proxmox_login_manager/extension.dart';
typedef OnLoginCallback = Function(proxclient.ProxmoxApiClient client);
body: FutureBuilder<ProxmoxLoginStorage>(
future: loginStorage,
builder: (context, snapshot) {
+ if (!snapshot.hasData) {
+ return Center(
+ child: CircularProgressIndicator(),
+ );
+ }
if (snapshot.hasData && (snapshot.data.logins?.isEmpty ?? true)) {
return Center(
child: Text('Add an account'),
);
}
+ var items = <Widget>[];
+ final logins = snapshot.data?.logins;
+ final activeSessions =
+ logins.rebuild((b) => b.where((b) => b.activeSession));
+
+ if (activeSessions.isNotEmpty) {
+ items.addAll([
+ Padding(
+ padding: const EdgeInsets.all(12.0),
+ child: Text(
+ 'Active Sessions',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ...activeSessions.map((s) => ListTile(
+ title: Text(s.fullHostname),
+ subtitle: Text(s.fullUsername),
+ trailing: Icon(Icons.navigate_next),
+ leading: PopupMenuButton(
+ icon: Icon(Icons.more_vert, color: Colors.green),
+ itemBuilder: (context) => [
+ PopupMenuItem(
+ child: ListTile(
+ dense: true,
+ leading: Icon(Icons.logout),
+ title: Text('Logout'),
+ onTap: () async {
+ await snapshot.data
+ .rebuild((b) => b.logins
+ .rebuildWhere((m) => s == m,
+ (b) => b..ticket = ''))
+ .saveToDisk();
+ refreshFromStorage();
+ Navigator.of(context).pop();
+ },
+ ),
+ ),
+ ]),
+ onTap: () => _login(user: s),
+ )),
+ ]);
+ }
+ items.addAll([
+ Padding(
+ padding: const EdgeInsets.all(12.0),
+ child: Text(
+ 'Available Sites',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ...logins.where((b) => !b.activeSession)?.map((l) => ListTile(
+ title: Text(l.fullHostname),
+ subtitle: Text(l.fullUsername),
+ trailing: Icon(Icons.navigate_next),
+ leading: PopupMenuButton(
+ itemBuilder: (context) => [
+ PopupMenuItem(
+ child: ListTile(
+ dense: true,
+ leading: Icon(Icons.delete),
+ title: Text('Delete'),
+ onTap: () async {
+ await snapshot.data
+ .rebuild((b) => b.logins.remove(l))
+ .saveToDisk();
+ refreshFromStorage();
+ Navigator.of(context).pop();
+ },
+ ),
+ ),
+ ]),
+ onTap: () => _login(user: l),
+ ))
+ ]);
return ListView(
- children: snapshot.data?.logins
- ?.map((l) => ListTile(
- title: Text(l.origin.host),
- subtitle: Text(l.fullUsername),
- trailing: Icon(Icons.navigate_next),
- leading: PopupMenuButton(
- itemBuilder: (context) => [
- PopupMenuItem(
- child: ListTile(
- dense: true,
- leading: Icon(Icons.delete),
- title: Text('Delete'),
- onTap: () {
- snapshot.data
- .rebuild(
- (b) => b.logins.remove(l))
- .saveToDisk();
- refreshFromStorage();
- Navigator.of(context).pop();
- },
- ),
- )
- ]),
- onTap: () => _login(user: l),
- ))
- ?.toList() ??
- [],
+ children: items,
);
}),
floatingActionButton: FloatingActionButton.extended(
builder: (context) => ProxmoxLoginPage(
userModel: user,
isCreate: isCreate,
+ ticket: user?.ticket,
)));
refreshFromStorage();
if (client != null) {
name: async
url: "https://pub.dartlang.org"
source: hosted
- version: "2.4.2"
+ version: "2.5.0-nullsafety"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.0"
+ version: "2.1.0-nullsafety"
build:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
- version: "1.1.3"
+ version: "1.2.0-nullsafety"
checked_yaml:
dependency: transitive
description:
name: clock
url: "https://pub.dartlang.org"
source: hosted
- version: "1.0.1"
+ version: "1.1.0-nullsafety"
code_builder:
dependency: transitive
description:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
- version: "1.1.0"
+ version: "1.1.0-nullsafety"
file:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
- version: "0.12.9"
+ version: "0.12.10-nullsafety"
meta:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
- version: "1.7.0"
+ version: "1.8.0-nullsafety"
path_provider_linux:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
- version: "1.7.0"
+ version: "1.8.0-nullsafety"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
- version: "1.9.5"
+ version: "1.10.0-nullsafety"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.0"
+ version: "2.1.0-nullsafety"
stream_transform:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
- version: "1.0.5"
+ version: "1.1.0-nullsafety"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
- version: "1.1.0"
+ version: "1.2.0-nullsafety"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
- version: "0.2.18"
+ version: "0.2.19-nullsafety"
timing:
dependency: transitive
description: