]> git.proxmox.com Git - flutter/proxmox_login_manager.git/commitdiff
add multi session usage
authorTim Marx <t.marx@proxmox.com>
Fri, 18 Sep 2020 10:56:49 +0000 (12:56 +0200)
committerTim Marx <t.marx@proxmox.com>
Fri, 18 Sep 2020 10:56:49 +0000 (12:56 +0200)
Signed-off-by: Tim Marx <t.marx@proxmox.com>
lib/extension.dart [new file with mode: 0644]
lib/proxmox_login_form.dart
lib/proxmox_login_model.dart
lib/proxmox_login_selector.dart
pubspec.lock

diff --git a/lib/extension.dart b/lib/extension.dart
new file mode 100644 (file)
index 0000000..785aba7
--- /dev/null
@@ -0,0 +1,11 @@
+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);
+    }
+  }
+}
index fc1cfa0e44ec2ffb91f6794b0091ae565c8c5122..343348a00d2774021e4a3983eda8da7becb019c5 100644 (file)
@@ -8,6 +8,7 @@ import 'package:proxmox_login_manager/proxmox_general_settings_form.dart';
 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;
@@ -50,16 +51,6 @@ class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> {
   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) {
@@ -163,11 +154,13 @@ class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> {
 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();
@@ -182,6 +175,7 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
   final _formKey = GlobalKey<FormState>();
   ProxmoxProgressModel _progressModel;
   bool _submittButtonEnabled = true;
+
   @override
   void initState() {
     super.initState();
@@ -195,6 +189,9 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
           '${userModel.origin?.host}:${userModel.origin?.port}';
       _accessDomains = _getAccessDomains();
       _usernameController.text = userModel.username;
+      if (widget.ticket.isNotEmpty) {
+        _onLoginButtonPressed(ticket: widget.ticket, mRealm: userModel.realm);
+      }
     }
   }
 
@@ -376,7 +373,8 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
     );
   }
 
-  Future<void> _onLoginButtonPressed() async {
+  Future<void> _onLoginButtonPressed(
+      {String ticket = '', String mRealm}) async {
     setState(() {
       _progressModel
         ..inProgress = true
@@ -389,8 +387,9 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
       //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);
@@ -403,6 +402,9 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
         ));
       }
 
+      final status = await client.getClusterStatus();
+      final hostname =
+          status.singleWhere((element) => element.local ?? false).name;
       var loginStorage = await ProxmoxLoginStorage.fromLocalStorage();
 
       if (widget.isCreate) {
@@ -411,14 +413,17 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
           ..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();
 
index f7ceccb7175e77bec62353e46d4d22233dc70e40..dfc1296298d15dfbea661e646a65b2d449bb6ed4 100644 (file)
@@ -73,6 +73,22 @@ abstract class ProxmoxLoginModel
   /// 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]) =
index 72770013445900417c0b972192ae86d8afb3ec65..f6ca2fac9d774abdd875f16aa75539a22da93a2b 100644 (file)
@@ -4,6 +4,7 @@ import 'package:proxmox_login_manager/proxmox_login_form.dart';
 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);
 
@@ -59,40 +60,99 @@ class _ProxmoxLoginSelectorState extends State<ProxmoxLoginSelector> {
         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(
@@ -109,6 +169,7 @@ class _ProxmoxLoginSelectorState extends State<ProxmoxLoginSelector> {
         builder: (context) => ProxmoxLoginPage(
               userModel: user,
               isCreate: isCreate,
+              ticket: user?.ticket,
             )));
     refreshFromStorage();
     if (client != null) {
index 87d06aa7f4f6036e6e84ca2daf5fd46d06015025..1d720492081c25ccaeaa0e0417445e4f8c505953 100644 (file)
@@ -28,14 +28,14 @@ packages:
       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:
@@ -112,7 +112,7 @@ packages:
       name: charcode
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.3"
+    version: "1.2.0-nullsafety"
   checked_yaml:
     dependency: transitive
     description:
@@ -133,7 +133,7 @@ packages:
       name: clock
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.0.1"
+    version: "1.1.0-nullsafety"
   code_builder:
     dependency: transitive
     description:
@@ -182,7 +182,7 @@ packages:
       name: fake_async
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.0"
+    version: "1.1.0-nullsafety"
   file:
     dependency: transitive
     description:
@@ -295,7 +295,7 @@ packages:
       name: matcher
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.12.9"
+    version: "0.12.10-nullsafety"
   meta:
     dependency: transitive
     description:
@@ -337,7 +337,7 @@ packages:
       name: path
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.7.0"
+    version: "1.8.0-nullsafety"
   path_provider_linux:
     dependency: transitive
     description:
@@ -489,21 +489,21 @@ packages:
       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:
@@ -517,21 +517,21 @@ packages:
       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: