]> git.proxmox.com Git - flutter/proxmox_login_manager.git/blobdiff - lib/proxmox_login_form.dart
tree wide: avoid using private types in public APIs for State
[flutter/proxmox_login_manager.git] / lib / proxmox_login_form.dart
index cffa7be742f0b8bb62e277cd3452894f15174433..5776ea516ea50796bdaa7be0c7bb387fa5dc1be7 100644 (file)
@@ -1,6 +1,8 @@
 import 'dart:io';
+import 'dart:async';
 
 import 'package:flutter/material.dart';
+import 'package:collection/src/iterable_extensions.dart';
 import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart'
     as proxclient;
 import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart';
@@ -8,140 +10,172 @@ 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';
+import 'package:proxmox_login_manager/proxmox_password_store.dart';
 
 class ProxmoxProgressModel {
-  bool inProgress;
-  String message;
+  int inProgress = 0;
+  String message = 'Loading...';
   ProxmoxProgressModel({
-    this.inProgress = false,
+    this.inProgress = 0,
     this.message = 'Loading...',
   });
 }
 
+// FIXME: copied from pve_flutter_frontend, re-use common set
+class ProxmoxColors {
+  static const Color orange = Color(0xFFE57000);
+  static const Color supportGrey = Color(0xFFABBABA);
+  static const Color supportBlue = Color(0xFF00617F);
+}
+
 class ProxmoxLoginForm extends StatefulWidget {
   final TextEditingController originController;
   final FormFieldValidator<String> originValidator;
   final TextEditingController usernameController;
   final TextEditingController passwordController;
-  final List<PveAccessDomainModel> accessDomains;
-  final PveAccessDomainModel selectedDomain;
-  final ValueChanged<PveAccessDomainModel> onDomainChanged;
+  final List<PveAccessDomainModel?>? accessDomains;
+  final PveAccessDomainModel? selectedDomain;
+  final ValueChanged<PveAccessDomainModel?> onDomainChanged;
+  final Function? onPasswordSubmitted;
+  final Function onOriginSubmitted;
+  final Function? onSavePasswordChanged;
+  final bool? canSavePassword;
+  final bool? passwordSaved;
 
   const ProxmoxLoginForm({
-    Key key,
-    @required this.originController,
-    @required this.usernameController,
-    @required this.passwordController,
-    @required this.accessDomains,
-    @required this.originValidator,
+    Key? key,
+    required this.originController,
+    required this.usernameController,
+    required this.passwordController,
+    required this.accessDomains,
+    required this.originValidator,
     this.selectedDomain,
-    @required this.onDomainChanged,
+    required this.onDomainChanged,
+    this.onPasswordSubmitted,
+    required this.onOriginSubmitted,
+    this.onSavePasswordChanged,
+    this.canSavePassword,
+    this.passwordSaved,
   }) : super(key: key);
 
   @override
-  _ProxmoxLoginFormState createState() => _ProxmoxLoginFormState();
+  State<ProxmoxLoginForm> createState() => _ProxmoxLoginFormState();
 }
 
 class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> {
   bool _obscure = true;
-  FocusNode passwordFocusNode;
-
-  @override
-  void initState() {
-    super.initState();
-
-    if (widget.usernameController.text.isNotEmpty) {
-      passwordFocusNode = FocusNode();
-      passwordFocusNode.requestFocus();
-    }
-  }
+  bool? _savePwCheckbox;
+  FocusNode? passwordFocusNode;
 
   @override
   Widget build(BuildContext context) {
     if (widget.accessDomains == null) {
       return TextFormField(
-        decoration: InputDecoration(
+        decoration: const InputDecoration(
             icon: Icon(Icons.vpn_lock),
             labelText: 'Origin',
             hintText: 'e.g. 192.168.1.2',
-            helperText: 'Protocol (https) and default port (8006) implied'),
+            helperText:
+                'Protocol (https) and default port (8006 or 443) implied'),
+        textInputAction: TextInputAction.next,
         controller: widget.originController,
         validator: widget.originValidator,
+        onFieldSubmitted: (value) => widget.onOriginSubmitted(),
       );
     }
 
-    return Column(
-      mainAxisAlignment: MainAxisAlignment.center,
-      children: [
-        TextFormField(
-          decoration: InputDecoration(
-            icon: Icon(Icons.vpn_lock),
-            labelText: 'Origin',
+    return AutofillGroup(
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: [
+          TextFormField(
+            decoration: const InputDecoration(
+              icon: Icon(Icons.vpn_lock),
+              labelText: 'Origin',
+            ),
+            controller: widget.originController,
+            enabled: false,
           ),
-          controller: widget.originController,
-          enabled: false,
-        ),
-        TextFormField(
-          decoration: InputDecoration(
-            icon: Icon(Icons.person),
-            labelText: 'Username',
+          TextFormField(
+            decoration: const InputDecoration(
+              icon: Icon(Icons.person),
+              labelText: 'Username',
+            ),
+            controller: widget.usernameController,
+            validator: (value) {
+              if (value!.isEmpty) {
+                return 'Please enter username';
+              }
+              return null;
+            },
+            autofillHints: const [AutofillHints.username],
           ),
-          controller: widget.usernameController,
-          validator: (value) {
-            if (value.isEmpty) {
-              return 'Please enter username';
-            }
-            return null;
-          },
-        ),
-        DropdownButtonFormField(
-          decoration: InputDecoration(icon: Icon(Icons.domain)),
-          items: widget.accessDomains
-              .map((e) => DropdownMenuItem(
-                    child: ListTile(
-                      title: Text(e.realm),
-                      subtitle: Text(e.comment ?? ''),
-                    ),
-                    value: e,
-                  ))
-              .toList(),
-          onChanged: widget.onDomainChanged,
-          selectedItemBuilder: (context) =>
-              widget.accessDomains.map((e) => Text(e.realm)).toList(),
-          value: widget.selectedDomain,
-        ),
-        Stack(
-          children: [
-            TextFormField(
-              decoration: InputDecoration(
-                icon: Icon(Icons.lock),
-                labelText: 'Password',
+          DropdownButtonFormField(
+            decoration: const InputDecoration(icon: Icon(Icons.domain)),
+            items: widget.accessDomains!
+                .map((e) => DropdownMenuItem(
+                      value: e,
+                      child: ListTile(
+                        title: Text(e!.realm),
+                        subtitle: Text(e.comment ?? ''),
+                      ),
+                    ))
+                .toList(),
+            onChanged: widget.onDomainChanged,
+            selectedItemBuilder: (context) =>
+                widget.accessDomains!.map((e) => Text(e!.realm)).toList(),
+            value: widget.selectedDomain,
+          ),
+          Stack(
+            children: [
+              TextFormField(
+                decoration: const InputDecoration(
+                  icon: Icon(Icons.lock),
+                  labelText: 'Password',
+                ),
+                controller: widget.passwordController,
+                obscureText: _obscure,
+                autocorrect: false,
+                focusNode: passwordFocusNode,
+                validator: (value) {
+                  if (value!.isEmpty) {
+                    return 'Please enter password';
+                  }
+                  return null;
+                },
+                onFieldSubmitted: (value) => widget.onPasswordSubmitted!(),
+                autofillHints: const [AutofillHints.password],
               ),
-              controller: widget.passwordController,
-              obscureText: _obscure,
-              autocorrect: false,
-              focusNode: passwordFocusNode,
-              validator: (value) {
-                if (value.isEmpty) {
-                  return 'Please enter password';
+              Align(
+                alignment: Alignment.bottomRight,
+                child: IconButton(
+                  constraints: BoxConstraints.tight(const Size(58, 58)),
+                  iconSize: 24,
+                  icon:
+                      Icon(_obscure ? Icons.visibility : Icons.visibility_off),
+                  onPressed: () => setState(() {
+                    _obscure = !_obscure;
+                  }),
+                ),
+              )
+            ],
+          ),
+          if (widget.canSavePassword ?? false)
+            CheckboxListTile(
+              title: const Text('Save password'),
+              value: _savePwCheckbox ?? widget.passwordSaved ?? false,
+              onChanged: (value) {
+                if (widget.onSavePasswordChanged != null) {
+                  widget.onSavePasswordChanged!(value!);
                 }
-                return null;
+                setState(() {
+                  _savePwCheckbox = value!;
+                });
               },
-            ),
-            Align(
-              alignment: Alignment.bottomRight,
-              child: IconButton(
-                constraints: BoxConstraints.tight(Size(58, 58)),
-                iconSize: 24,
-                icon: Icon(_obscure ? Icons.visibility : Icons.visibility_off),
-                onPressed: () => setState(() {
-                  _obscure = !_obscure;
-                }),
-              ),
             )
-          ],
-        ),
-      ],
+        ],
+      ),
     );
   }
 
@@ -153,187 +187,264 @@ class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> {
 }
 
 class ProxmoxLoginPage extends StatefulWidget {
-  final ProxmoxLoginModel userModel;
-  final bool isCreate;
+  final ProxmoxLoginModel? userModel;
+  final bool? isCreate;
+  final String? ticket;
+  final String? password;
 
   const ProxmoxLoginPage({
-    Key key,
+    Key? key,
     this.userModel,
     this.isCreate,
+    this.ticket = '',
+    this.password,
   }) : super(key: key);
   @override
-  _ProxmoxLoginPageState createState() => _ProxmoxLoginPageState();
+  State<ProxmoxLoginPage> createState() => _ProxmoxLoginPageState();
 }
 
 class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
   final _originController = TextEditingController();
   final _usernameController = TextEditingController();
   final _passwordController = TextEditingController();
-  Future<List<PveAccessDomainModel>> _accessDomains;
-  PveAccessDomainModel _selectedDomain;
+  Future<List<PveAccessDomainModel?>?>? _accessDomains;
+  PveAccessDomainModel? _selectedDomain;
   final _formKey = GlobalKey<FormState>();
-  ProxmoxProgressModel _progressModel;
+  ProxmoxProgressModel _progressModel = ProxmoxProgressModel();
   bool _submittButtonEnabled = true;
+  bool _canSavePassword = false;
+  bool _savePasswordCB = false;
+
   @override
   void initState() {
     super.initState();
     final userModel = widget.userModel;
     _progressModel = ProxmoxProgressModel();
-    if (!widget.isCreate && userModel != null) {
-      _progressModel
-        ..inProgress = true
-        ..message = 'Connection test...';
-      _originController.text =
-          '${userModel.origin?.host}:${userModel.origin?.port}';
+    if (!widget.isCreate! && userModel != null) {
+      _originController.text = userModel.origin?.toString() ?? '';
+      // Uri does not append 443 for https, so we do it manually
+      if (userModel.origin != null &&
+          userModel.origin!.scheme == "https" &&
+          userModel.origin!.port == 443) {
+        _originController.text += ":443";
+      }
+      _passwordController.text = widget.password ?? '';
       _accessDomains = _getAccessDomains();
-      _usernameController.text = userModel.username;
+      _usernameController.text = userModel.username!;
+      _savePasswordCB = widget.password != null;
+      if ((widget.ticket!.isNotEmpty && userModel.activeSession) ||
+          widget.password != null) {
+        _onLoginButtonPressed(ticket: widget.ticket!, mRealm: userModel.realm);
+      }
     }
   }
 
   @override
   Widget build(BuildContext context) {
     return Theme(
-      data: ThemeData.dark().copyWith(accentColor: Color(0xFFE47225)),
-      child: Material(
-        color: Theme.of(context).primaryColor,
-        child: Stack(
+      //data: ThemeData.dark().copyWith(accentColor: Color(0xFFE47225)),
+      data: ThemeData.dark().copyWith(
+        textButtonTheme: TextButtonThemeData(
+          style: TextButton.styleFrom(
+            foregroundColor: Colors.white,
+            backgroundColor: ProxmoxColors.orange,
+            disabledBackgroundColor: Colors.grey,
+          ),
+        ),
+        colorScheme: const ColorScheme.dark().copyWith(
+            primary: ProxmoxColors.orange,
+            secondary: ProxmoxColors.orange,
+            onSecondary: ProxmoxColors.supportGrey),
+        checkboxTheme: CheckboxThemeData(
+          fillColor: MaterialStateProperty.resolveWith<Color?>(
+              (Set<MaterialState> states) {
+            if (states.contains(MaterialState.disabled)) {
+              return null;
+            }
+            if (states.contains(MaterialState.selected)) {
+              return ProxmoxColors.orange;
+            }
+            return null;
+          }),
+        ),
+        radioTheme: RadioThemeData(
+          fillColor: MaterialStateProperty.resolveWith<Color?>(
+              (Set<MaterialState> states) {
+            if (states.contains(MaterialState.disabled)) {
+              return null;
+            }
+            if (states.contains(MaterialState.selected)) {
+              return ProxmoxColors.orange;
+            }
+            return null;
+          }),
+        ),
+        switchTheme: SwitchThemeData(
+          thumbColor: MaterialStateProperty.resolveWith<Color?>(
+              (Set<MaterialState> states) {
+            if (states.contains(MaterialState.disabled)) {
+              return null;
+            }
+            if (states.contains(MaterialState.selected)) {
+              return ProxmoxColors.orange;
+            }
+            return null;
+          }),
+          trackColor: MaterialStateProperty.resolveWith<Color?>(
+              (Set<MaterialState> states) {
+            if (states.contains(MaterialState.disabled)) {
+              return null;
+            }
+            if (states.contains(MaterialState.selected)) {
+              return ProxmoxColors.orange;
+            }
+            return null;
+          }),
+        ),
+      ),
+      child: Scaffold(
+        backgroundColor: ProxmoxColors.supportBlue,
+        extendBodyBehindAppBar: true,
+        appBar: AppBar(
+          elevation: 0.0,
+          backgroundColor: Colors.transparent,
+          leading: IconButton(
+            icon: const Icon(Icons.close),
+            onPressed: () => Navigator.of(context).pop(),
+          ),
+        ),
+        body: Stack(
           children: [
             SingleChildScrollView(
               child: ConstrainedBox(
                 constraints: BoxConstraints.tightFor(
                     height: MediaQuery.of(context).size.height),
-                child: Padding(
-                  padding: const EdgeInsets.all(8.0),
-                  child: FutureBuilder<List<PveAccessDomainModel>>(
-                      future: _accessDomains,
-                      builder: (context, snapshot) {
-                        return Form(
-                          key: _formKey,
-                          onChanged: () {
-                            setState(() {
-                              _submittButtonEnabled =
-                                  _formKey.currentState.validate();
-                            });
-                          },
-                          child: Column(
-                            mainAxisAlignment: MainAxisAlignment.center,
-                            children: [
-                              Expanded(
-                                child: Container(
-                                  child: Column(
-                                    mainAxisAlignment: MainAxisAlignment.center,
-                                    children: [
-                                      Text(
-                                        'PROXMOX',
-                                        style: TextStyle(
-                                          fontFamily: 'Proxmox',
-                                          fontSize: 36,
-                                        ),
-                                      ),
-                                      Text(
-                                        'Open Source',
-                                        style: TextStyle(
-                                          fontSize: 18,
-                                        ),
-                                      ),
-                                    ],
-                                  ),
-                                ),
-                              ),
-                              ProxmoxLoginForm(
-                                originController: _originController,
-                                originValidator: (value) {
-                                  if (value.isEmpty) {
-                                    return 'Please enter origin';
-                                  }
-                                  if (value.startsWith('https://') ||
-                                      value.startsWith('http://')) {
-                                    return 'Do not prefix with scheme';
-                                  }
-                                  try {
-                                    Uri.https(value, '');
-                                    return null;
-                                  } on FormatException catch (_) {
-                                    return 'Invalid URI';
-                                  }
-                                },
-                                usernameController: _usernameController,
-                                passwordController: _passwordController,
-                                accessDomains: snapshot.data,
-                                selectedDomain: _selectedDomain,
-                                onDomainChanged: (value) {
-                                  setState(() {
-                                    _selectedDomain = value;
-                                  });
-                                },
-                              ),
-                              if (snapshot.hasData)
+                child: SafeArea(
+                  child: Padding(
+                    padding: const EdgeInsets.all(8.0),
+                    child: FutureBuilder<List<PveAccessDomainModel?>?>(
+                        future: _accessDomains,
+                        builder: (context, snapshot) {
+                          return Form(
+                            key: _formKey,
+                            onChanged: () {
+                              setState(() {
+                                _submittButtonEnabled =
+                                    _formKey.currentState!.validate();
+                              });
+                            },
+                            child: Column(
+                              mainAxisAlignment: MainAxisAlignment.center,
+                              children: [
                                 Expanded(
-                                  child: Align(
-                                    alignment: Alignment.bottomCenter,
-                                    child: Container(
-                                      width: MediaQuery.of(context).size.width,
-                                      child: FlatButton(
-                                        onPressed: _submittButtonEnabled
-                                            ? () {
-                                                final isValid = _formKey
-                                                    .currentState
-                                                    .validate();
-                                                setState(() {
-                                                  _submittButtonEnabled =
-                                                      isValid;
-                                                });
-                                                if (isValid) {
-                                                  _onLoginButtonPressed();
-                                                }
-                                              }
-                                            : null,
-                                        color: Color(0xFFE47225),
-                                        disabledColor: Colors.grey,
-                                        child: Text('Continue'),
-                                      ),
+                                  child: Container(
+                                    child: Column(
+                                      mainAxisAlignment:
+                                          MainAxisAlignment.center,
+                                      children: [
+                                        Image.asset(
+                                          'assets/images/proxmox_logo_symbol_wordmark.png',
+                                          package: 'proxmox_login_manager',
+                                        ),
+                                      ],
                                     ),
                                   ),
                                 ),
-                              if (!snapshot.hasData)
+                                ProxmoxLoginForm(
+                                  originController: _originController,
+                                  originValidator: (value) {
+                                    if (value == null || value.isEmpty) {
+                                      return 'Please enter origin';
+                                    }
+                                    try {
+                                      normalizeUrl(value);
+                                      return null;
+                                    } on FormatException catch (_) {
+                                      return 'Invalid URI';
+                                    } on Exception catch (e) {
+                                      return 'Invalid URI: $e';
+                                    }
+                                  },
+                                  usernameController: _usernameController,
+                                  passwordController: _passwordController,
+                                  accessDomains: snapshot.data,
+                                  selectedDomain: _selectedDomain,
+                                  onSavePasswordChanged: (value) {
+                                    _savePasswordCB = value;
+                                  },
+                                  canSavePassword: _canSavePassword,
+                                  passwordSaved: widget.password != null,
+                                  onDomainChanged: (value) {
+                                    setState(() {
+                                      _selectedDomain = value;
+                                    });
+                                  },
+                                  onOriginSubmitted: () {
+                                    final isValid =
+                                        _formKey.currentState!.validate();
+                                    setState(() {
+                                      _submittButtonEnabled = isValid;
+                                    });
+                                    if (isValid) {
+                                      setState(() {
+                                        _accessDomains = _getAccessDomains();
+                                      });
+                                    }
+                                  },
+                                  onPasswordSubmitted: _submittButtonEnabled
+                                      ? () {
+                                          final isValid =
+                                              _formKey.currentState!.validate();
+                                          setState(() {
+                                            _submittButtonEnabled = isValid;
+                                          });
+                                          if (isValid) {
+                                            _onLoginButtonPressed();
+                                          }
+                                        }
+                                      : null,
+                                ),
                                 Expanded(
                                   child: Align(
                                     alignment: Alignment.bottomCenter,
-                                    child: Container(
+                                    child: SizedBox(
                                       width: MediaQuery.of(context).size.width,
-                                      child: FlatButton(
+                                      child: TextButton(
                                         onPressed: _submittButtonEnabled
                                             ? () {
                                                 final isValid = _formKey
-                                                    .currentState
+                                                    .currentState!
                                                     .validate();
                                                 setState(() {
                                                   _submittButtonEnabled =
                                                       isValid;
                                                 });
                                                 if (isValid) {
-                                                  setState(() {
-                                                    _accessDomains =
-                                                        _getAccessDomains();
-                                                  });
+                                                  if (snapshot.hasData) {
+                                                    _onLoginButtonPressed();
+                                                  } else {
+                                                    setState(() {
+                                                      _accessDomains =
+                                                          _getAccessDomains();
+                                                    });
+                                                  }
                                                 }
                                               }
                                             : null,
-                                        color: Color(0xFFE47225),
-                                        child: Text('Continue'),
-                                        disabledColor: Colors.grey,
+                                        child: const Text('Continue'),
                                       ),
                                     ),
                                   ),
                                 ),
-                            ],
-                          ),
-                        );
-                      }),
+                              ],
+                            ),
+                          );
+                        }),
+                  ),
                 ),
               ),
             ),
-            if (_progressModel.inProgress)
+            if (_progressModel.inProgress > 0)
               ProxmoxProgressOverlay(message: _progressModel.message),
           ],
         ),
@@ -341,130 +452,236 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
     );
   }
 
-  Future<void> _onLoginButtonPressed() async {
+  Future<void> _onLoginButtonPressed(
+      {String ticket = '', String? mRealm}) async {
     setState(() {
       _progressModel
-        ..inProgress = true
+        ..inProgress += 1
         ..message = 'Authenticating...';
     });
 
     try {
       final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
-      final origin = Uri.https(_originController.text, '');
+      //cleaned form fields
+      final origin = normalizeUrl(_originController.text.trim());
+      final username = _usernameController.text.trim();
+      final String enteredPassword = _passwordController.text.trim();
+      final String? savedPassword = widget.password;
+
+      final password = ticket.isNotEmpty ? ticket : enteredPassword;
+      final realm = _selectedDomain?.realm ?? mRealm;
+
       var client = await proxclient.authenticate(
-          '${_usernameController.text}@${_selectedDomain.realm}',
-          _passwordController.text,
-          origin,
-          settings.sslValidation);
+          '$username@$realm', password, origin, settings.sslValidation!);
 
-      if (client.credentials.tfa) {
-        client = await Navigator.of(context).push(MaterialPageRoute(
+      if (client.credentials.tfa != null &&
+          client.credentials.tfa!.kinds().isNotEmpty) {
+        if (!mounted) return;
+        ProxmoxApiClient? tfaclient =
+            await Navigator.of(context).push(MaterialPageRoute(
           builder: (context) => ProxmoxTfaForm(
             apiClient: client,
           ),
         ));
+
+        if (tfaclient != null) {
+          client = tfaclient;
+        } else {
+          setState(() {
+            _progressModel.inProgress -= 1;
+          });
+          return;
+        }
       }
 
+      final status = await client.getClusterStatus();
+      final hostname =
+          status.singleWhereOrNull((element) => element.local ?? false)?.name;
       var loginStorage = await ProxmoxLoginStorage.fromLocalStorage();
 
-      if (widget.isCreate) {
+      final savePW = enteredPassword != '' &&
+          _savePasswordCB &&
+          enteredPassword != savedPassword;
+      final deletePW = enteredPassword != '' && !savePW && !_savePasswordCB;
+      String? id;
+
+      if (widget.isCreate!) {
         final newLogin = ProxmoxLoginModel((b) => b
           ..origin = origin
-          ..username = _usernameController.text
-          ..realm = _selectedDomain.realm
+          ..username = username
+          ..realm = realm
           ..productType = ProxmoxProductType.pve
-          ..ticket = client.credentials.ticket);
-        loginStorage = loginStorage.rebuild((b) => b..logins.add(newLogin));
+          ..ticket = client.credentials.ticket
+          ..passwordSaved = savePW
+          ..hostname = hostname);
+
+        loginStorage = loginStorage!.rebuild((b) => b..logins.add(newLogin));
+        id = newLogin.identifier;
       } else {
-        loginStorage = loginStorage.rebuild((b) => b
-          ..logins.remove(widget.userModel)
-          ..logins.add(widget.userModel
-              .rebuild((b) => b..ticket = client.credentials.ticket)));
+        loginStorage = loginStorage!.rebuild((b) => b
+          ..logins.rebuildWhere(
+              (m) => m == widget.userModel,
+              (b) => b
+                ..ticket = client.credentials.ticket
+                ..passwordSaved =
+                    savePW || (deletePW ? false : b.passwordSaved ?? false)
+                ..hostname = hostname));
+        id = widget.userModel!.identifier;
+      }
+
+      if (id != null) {
+        if (savePW) {
+          await savePassword(id, enteredPassword);
+        } else if (deletePW) {
+          await deletePassword(id);
+        }
       }
       await loginStorage.saveToDisk();
 
-      Navigator.of(context).pop(client);
+      if (mounted) {
+        Navigator.of(context).pop(client);
+      }
     } on proxclient.ProxmoxApiException catch (e) {
       print(e);
-      showDialog(
-        context: context,
-        builder: (context) => ProxmoxApiErrorDialog(
-          exception: e,
-        ),
-      );
-    } catch (e, trace) {
-      print(e);
-      print(trace);
-      if (e.runtimeType == HandshakeException) {
+      if (!mounted) return;
+      if (e.message.contains('No ticket')) {
+        showDialog(
+          context: context,
+          builder: (context) => AlertDialog(
+            title: const Text('Version Error'),
+            content: const Text(
+                'Proxmox VE version not supported, please update your instance to use this app.'),
+            actions: [
+              TextButton(
+                onPressed: () => Navigator.of(context).pop(),
+                child: const Text('Close'),
+              ),
+            ],
+          ),
+        );
+      } else {
         showDialog(
           context: context,
-          builder: (context) => ProxmoxCertificateErrorDialog(),
+          builder: (context) => ProxmoxApiErrorDialog(
+            exception: e,
+          ),
         );
       }
+    } catch (e, trace) {
+      print(e);
+      print(trace);
+      if (mounted) {
+        if (e.runtimeType == HandshakeException) {
+          showDialog(
+            context: context,
+            builder: (context) => const ProxmoxCertificateErrorDialog(),
+          );
+        } else {
+          showDialog(
+            context: context,
+            builder: (context) => ConnectionErrorDialog(exception: e),
+          );
+        }
+      }
     }
     setState(() {
-      _progressModel.inProgress = false;
+      _progressModel.inProgress -= 1;
     });
   }
 
-  Future<List<PveAccessDomainModel>> _getAccessDomains() async {
-    setState(() {
-      _progressModel
-        ..inProgress = true
-        ..message = 'Connection test...';
-    });
-    var apiBaseUrl = Uri.https(_originController.text, '');
-
-    if (!apiBaseUrl.hasPort) {
-      _originController.text += ':8006';
-      apiBaseUrl = apiBaseUrl.replace(port: 8006);
-    }
-
+  Future<List<PveAccessDomainModel?>?> _loadAccessDomains(Uri uri) async {
     final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
-    List<PveAccessDomainModel> response;
+    List<PveAccessDomainModel?>? response;
     try {
-      response =
-          await proxclient.accessDomains(apiBaseUrl, settings.sslValidation);
+      response = await proxclient.accessDomains(uri, settings.sslValidation!);
     } on proxclient.ProxmoxApiException catch (e) {
-      print(e);
-      showDialog(
-        context: context,
-        builder: (context) => ProxmoxApiErrorDialog(
-          exception: e,
-        ),
-      );
-    } catch (e, trace) {
-      print(e);
-      print(trace);
-      if (e.runtimeType == HandshakeException) {
+      if (mounted) {
         showDialog(
           context: context,
-          builder: (context) => ProxmoxCertificateErrorDialog(),
+          builder: (context) => ProxmoxApiErrorDialog(
+            exception: e,
+          ),
         );
-      } else {
+      }
+    } on HandshakeException {
+      if (mounted) {
         showDialog(
           context: context,
-          builder: (context) => AlertDialog(
-            title: Text('Connection error'),
-            content: Text('Could not establish connection.'),
-            actions: [
-              FlatButton(
-                onPressed: () => Navigator.of(context).pop(),
-                child: Text('Close'),
-              ),
-            ],
+          builder: (context) => const ProxmoxCertificateErrorDialog(),
+        );
+      }
+    }
+    return response;
+  }
+
+  Future<List<PveAccessDomainModel?>?> _tryLoadAccessDomains(Uri uri) async {
+    List<PveAccessDomainModel?>? response;
+    try {
+      response = await _loadAccessDomains(uri);
+    } catch (e) {
+      if (mounted) {
+        showDialog(
+          context: context,
+          builder: (context) => ConnectionErrorDialog(
+            exception: e,
           ),
         );
       }
     }
+    return response;
+  }
+
+  Future<List<PveAccessDomainModel?>?> _getAccessDomains() async {
+    setState(() {
+      _progressModel
+        ..inProgress += 1
+        ..message = 'Connecting...';
+    });
+
+    final canSavePW = await canSavePassword();
+    setState(() {
+      _canSavePassword = canSavePW;
+    });
+
+    var host = _originController.text.trim();
+    var apiBaseUrl = normalizeUrl(host);
+
+    RegExp portRE = RegExp(r":\d{1,5}$");
+
+    List<PveAccessDomainModel?>? response;
+
+    if (portRE.hasMatch(host)) {
+      response = await _tryLoadAccessDomains(apiBaseUrl);
+    } else {
+      // try to guess the port, 8006 first, and then 443
+      apiBaseUrl = apiBaseUrl.replace(port: 8006);
+      try {
+        response = await _loadAccessDomains(apiBaseUrl);
+        if (response != null) {
+          _originController.text = '$host:8006';
+        }
+      } catch (e) {
+        // we were no port given, and we couldn't reach on port 8006, retry with 443
+        apiBaseUrl = apiBaseUrl.replace(port: 443);
+        response = await _tryLoadAccessDomains(apiBaseUrl);
+        if (response != null) {
+          _originController.text = '$host:443';
+        }
+      }
+    }
+
+    response?.sort((a, b) => a!.realm.compareTo(b!.realm));
+
     final selection = response?.singleWhere(
-      (e) => e.realm == widget.userModel?.realm,
+      (e) => e!.realm == widget.userModel?.realm,
       orElse: () => response?.first,
     );
+
     setState(() {
-      _progressModel.inProgress = false;
+      _progressModel.inProgress -= 1;
       _selectedDomain = selection;
     });
+
     return response;
   }
 
@@ -479,8 +696,8 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
 
 class ProxmoxProgressOverlay extends StatelessWidget {
   const ProxmoxProgressOverlay({
-    Key key,
-    @required this.message,
+    Key? key,
+    required this.message,
   }) : super(key: key);
 
   final String message;
@@ -488,20 +705,19 @@ class ProxmoxProgressOverlay extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return Container(
-      decoration: new BoxDecoration(color: Colors.black.withOpacity(0.5)),
+      decoration: BoxDecoration(color: Colors.black.withOpacity(0.5)),
       child: Center(
         child: Column(
           mainAxisAlignment: MainAxisAlignment.center,
           children: [
             Text(
               message,
-              style: TextStyle(
-                color: Theme.of(context).accentColor,
+              style: const TextStyle(
                 fontSize: 20,
               ),
             ),
-            Padding(
-              padding: const EdgeInsets.only(top: 20.0),
+            const Padding(
+              padding: EdgeInsets.only(top: 20.0),
               child: CircularProgressIndicator(),
             )
           ],
@@ -511,25 +727,48 @@ class ProxmoxProgressOverlay extends StatelessWidget {
   }
 }
 
+class ConnectionErrorDialog extends StatelessWidget {
+  final Object exception;
+
+  const ConnectionErrorDialog({
+    Key? key,
+    required this.exception,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return AlertDialog(
+      title: const Text('Connection error'),
+      content: Text('Could not establish connection: $exception'),
+      actions: [
+        TextButton(
+          onPressed: () => Navigator.of(context).pop(),
+          child: const Text('Close'),
+        ),
+      ],
+    );
+  }
+}
+
 class ProxmoxApiErrorDialog extends StatelessWidget {
   final proxclient.ProxmoxApiException exception;
 
   const ProxmoxApiErrorDialog({
-    Key key,
-    @required this.exception,
+    Key? key,
+    required this.exception,
   }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     return AlertDialog(
-      title: Text('API Error'),
+      title: const Text('API Error'),
       content: SingleChildScrollView(
         child: Text(exception.message),
       ),
       actions: [
-        FlatButton(
+        TextButton(
           onPressed: () => Navigator.of(context).pop(),
-          child: Text('Close'),
+          child: const Text('Close'),
         ),
       ],
     );
@@ -538,38 +777,49 @@ class ProxmoxApiErrorDialog extends StatelessWidget {
 
 class ProxmoxCertificateErrorDialog extends StatelessWidget {
   const ProxmoxCertificateErrorDialog({
-    Key key,
+    Key? key,
   }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     return AlertDialog(
-      title: Text('Certificate error'),
+      title: const Text('Certificate error'),
       content: SingleChildScrollView(
         child: Column(
           crossAxisAlignment: CrossAxisAlignment.start,
           children: [
-            Text('Your connection is not private.'),
+            const Text('Your connection is not private.'),
             Text(
               'Note: Consider to disable SSL validation,'
               ' if you use a self signed, not commonly trusted, certificate.',
-              style: Theme.of(context).textTheme.caption,
+              style: Theme.of(context).textTheme.bodySmall,
             ),
           ],
         ),
       ),
       actions: [
-        FlatButton(
+        TextButton(
           onPressed: () => Navigator.of(context).pop(),
-          child: Text('Close'),
+          child: const Text('Close'),
         ),
-        FlatButton(
+        TextButton(
           onPressed: () => Navigator.of(context).pushReplacement(
               MaterialPageRoute(
-                  builder: (context) => ProxmoxGeneralSettingsForm())),
-          child: Text('Settings'),
+                  builder: (context) => const ProxmoxGeneralSettingsForm())),
+          child: const Text('Settings'),
         )
       ],
     );
   }
 }
+
+Uri normalizeUrl(String urlText) {
+  if (urlText.startsWith('https://')) {
+    urlText = urlText.substring('https://'.length);
+  }
+  if (urlText.startsWith('http://')) {
+    throw Exception("HTTP without TLS is not supported");
+  }
+
+  return Uri.https(urlText, '');
+}