]> 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 2609f71fd25144e9306f0af36ecf19fe5245e7d7..5776ea516ea50796bdaa7be0c7bb387fa5dc1be7 100644 (file)
@@ -11,21 +11,22 @@ 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 = false;
+  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 final Color orange = Color(0xFFE57000);
-  static final Color supportGrey = Color(0xFFABBABA);
-  static final Color supportBlue = Color(0xFF00617F);
+  static const Color orange = Color(0xFFE57000);
+  static const Color supportGrey = Color(0xFFABBABA);
+  static const Color supportBlue = Color(0xFF00617F);
 }
 
 class ProxmoxLoginForm extends StatefulWidget {
@@ -37,7 +38,10 @@ class ProxmoxLoginForm extends StatefulWidget {
   final PveAccessDomainModel? selectedDomain;
   final ValueChanged<PveAccessDomainModel?> onDomainChanged;
   final Function? onPasswordSubmitted;
-  final Function? onOriginSubmitted;
+  final Function onOriginSubmitted;
+  final Function? onSavePasswordChanged;
+  final bool? canSavePassword;
+  final bool? passwordSaved;
 
   const ProxmoxLoginForm({
     Key? key,
@@ -49,29 +53,35 @@ class ProxmoxLoginForm extends StatefulWidget {
     this.selectedDomain,
     required this.onDomainChanged,
     this.onPasswordSubmitted,
-    this.onOriginSubmitted,
+    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;
+  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!(),
+        onFieldSubmitted: (value) => widget.onOriginSubmitted(),
       );
     }
 
@@ -80,7 +90,7 @@ class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> {
         mainAxisAlignment: MainAxisAlignment.center,
         children: [
           TextFormField(
-            decoration: InputDecoration(
+            decoration: const InputDecoration(
               icon: Icon(Icons.vpn_lock),
               labelText: 'Origin',
             ),
@@ -88,7 +98,7 @@ class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> {
             enabled: false,
           ),
           TextFormField(
-            decoration: InputDecoration(
+            decoration: const InputDecoration(
               icon: Icon(Icons.person),
               labelText: 'Username',
             ),
@@ -99,17 +109,17 @@ class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> {
               }
               return null;
             },
-            autofillHints: [AutofillHints.username],
+            autofillHints: const [AutofillHints.username],
           ),
           DropdownButtonFormField(
-            decoration: InputDecoration(icon: Icon(Icons.domain)),
+            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 ?? ''),
                       ),
-                      value: e,
                     ))
                 .toList(),
             onChanged: widget.onDomainChanged,
@@ -120,7 +130,7 @@ class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> {
           Stack(
             children: [
               TextFormField(
-                decoration: InputDecoration(
+                decoration: const InputDecoration(
                   icon: Icon(Icons.lock),
                   labelText: 'Password',
                 ),
@@ -135,12 +145,12 @@ class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> {
                   return null;
                 },
                 onFieldSubmitted: (value) => widget.onPasswordSubmitted!(),
-                autofillHints: [AutofillHints.password],
+                autofillHints: const [AutofillHints.password],
               ),
               Align(
                 alignment: Alignment.bottomRight,
                 child: IconButton(
-                  constraints: BoxConstraints.tight(Size(58, 58)),
+                  constraints: BoxConstraints.tight(const Size(58, 58)),
                   iconSize: 24,
                   icon:
                       Icon(_obscure ? Icons.visibility : Icons.visibility_off),
@@ -151,6 +161,19 @@ class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> {
               )
             ],
           ),
+          if (widget.canSavePassword ?? false)
+            CheckboxListTile(
+              title: const Text('Save password'),
+              value: _savePwCheckbox ?? widget.passwordSaved ?? false,
+              onChanged: (value) {
+                if (widget.onSavePasswordChanged != null) {
+                  widget.onSavePasswordChanged!(value!);
+                }
+                setState(() {
+                  _savePwCheckbox = value!;
+                });
+              },
+            )
         ],
       ),
     );
@@ -167,15 +190,17 @@ class ProxmoxLoginPage extends StatefulWidget {
   final ProxmoxLoginModel? userModel;
   final bool? isCreate;
   final String? ticket;
+  final String? password;
 
   const ProxmoxLoginPage({
     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> {
@@ -187,6 +212,8 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
   final _formKey = GlobalKey<FormState>();
   ProxmoxProgressModel _progressModel = ProxmoxProgressModel();
   bool _submittButtonEnabled = true;
+  bool _canSavePassword = false;
+  bool _savePasswordCB = false;
 
   @override
   void initState() {
@@ -194,14 +221,19 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
     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}';
+      _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!;
-      if (widget.ticket!.isNotEmpty && userModel.activeSession) {
+      _savePasswordCB = widget.password != null;
+      if ((widget.ticket!.isNotEmpty && userModel.activeSession) ||
+          widget.password != null) {
         _onLoginButtonPressed(ticket: widget.ticket!, mRealm: userModel.realm);
       }
     }
@@ -212,9 +244,64 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
     return Theme(
       //data: ThemeData.dark().copyWith(accentColor: Color(0xFFE47225)),
       data: ThemeData.dark().copyWith(
-          colorScheme: ColorScheme.dark().copyWith(
-              secondary: ProxmoxColors.orange,
-              onSecondary: ProxmoxColors.supportGrey)),
+        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,
@@ -222,7 +309,7 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
           elevation: 0.0,
           backgroundColor: Colors.transparent,
           leading: IconButton(
-            icon: Icon(Icons.close),
+            icon: const Icon(Icons.close),
             onPressed: () => Navigator.of(context).pop(),
           ),
         ),
@@ -232,124 +319,97 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
               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: [
-                                      Image.asset(
-                                        'assets/images/proxmox_logo_symbol_wordmark.png',
-                                        package: 'proxmox_login_manager',
-                                      ),
-                                    ],
+                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: Container(
+                                    child: Column(
+                                      mainAxisAlignment:
+                                          MainAxisAlignment.center,
+                                      children: [
+                                        Image.asset(
+                                          'assets/images/proxmox_logo_symbol_wordmark.png',
+                                          package: 'proxmox_login_manager',
+                                        ),
+                                      ],
+                                    ),
                                   ),
                                 ),
-                              ),
-                              ProxmoxLoginForm(
-                                originController: _originController,
-                                originValidator: (value) {
-                                  if (value == null || 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;
-                                  });
-                                },
-                                onOriginSubmitted: _submittButtonEnabled
-                                    ? () {
-                                        final isValid =
-                                            _formKey.currentState!.validate();
-                                        setState(() {
-                                          _submittButtonEnabled = isValid;
-                                        });
-                                        if (isValid) {
+                                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(() {
-                                            _accessDomains =
-                                                _getAccessDomains();
+                                            _submittButtonEnabled = isValid;
                                           });
+                                          if (isValid) {
+                                            _onLoginButtonPressed();
+                                          }
                                         }
-                                      }
-                                    : null,
-                                onPasswordSubmitted: _submittButtonEnabled
-                                    ? () {
-                                        final isValid =
-                                            _formKey.currentState!.validate();
-                                        setState(() {
-                                          _submittButtonEnabled = isValid;
-                                        });
-                                        if (isValid) {
-                                          _onLoginButtonPressed();
-                                        }
-                                      }
-                                    : null,
-                              ),
-                              if (snapshot.hasData)
-                                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: ProxmoxColors.orange,
-                                        disabledColor: Colors.grey,
-                                        child: Text('Continue'),
-                                      ),
-                                    ),
-                                  ),
+                                      : null,
                                 ),
-                              if (!snapshot.hasData)
                                 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
@@ -360,28 +420,31 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
                                                       isValid;
                                                 });
                                                 if (isValid) {
-                                                  setState(() {
-                                                    _accessDomains =
-                                                        _getAccessDomains();
-                                                  });
+                                                  if (snapshot.hasData) {
+                                                    _onLoginButtonPressed();
+                                                  } else {
+                                                    setState(() {
+                                                      _accessDomains =
+                                                          _getAccessDomains();
+                                                    });
+                                                  }
                                                 }
                                               }
                                             : null,
-                                        color: ProxmoxColors.orange,
-                                        child: Text('Continue'),
-                                        disabledColor: Colors.grey,
+                                        child: const Text('Continue'),
                                       ),
                                     ),
                                   ),
                                 ),
-                            ],
-                          ),
-                        );
-                      }),
+                              ],
+                            ),
+                          );
+                        }),
+                  ),
                 ),
               ),
             ),
-            if (_progressModel.inProgress)
+            if (_progressModel.inProgress > 0)
               ProxmoxProgressOverlay(message: _progressModel.message),
           ],
         ),
@@ -393,29 +456,42 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
       {String ticket = '', String? mRealm}) async {
     setState(() {
       _progressModel
-        ..inProgress = true
+        ..inProgress += 1
         ..message = 'Authenticating...';
     });
 
     try {
       final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
-
       //cleaned form fields
-      final origin = Uri.https(_originController.text.trim(), '');
+      final origin = normalizeUrl(_originController.text.trim());
       final username = _usernameController.text.trim();
-      final password =
-          ticket.isNotEmpty ? ticket : _passwordController.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(
           '$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();
@@ -423,6 +499,12 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
           status.singleWhereOrNull((element) => element.local ?? false)?.name;
       var loginStorage = await ProxmoxLoginStorage.fromLocalStorage();
 
+      final savePW = enteredPassword != '' &&
+          _savePasswordCB &&
+          enteredPassword != savedPassword;
+      final deletePW = enteredPassword != '' && !savePW && !_savePasswordCB;
+      String? id;
+
       if (widget.isCreate!) {
         final newLogin = ProxmoxLoginModel((b) => b
           ..origin = origin
@@ -430,33 +512,49 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
           ..realm = realm
           ..productType = ProxmoxProductType.pve
           ..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.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);
+      if (!mounted) return;
       if (e.message.contains('No ticket')) {
         showDialog(
           context: context,
           builder: (context) => AlertDialog(
-            title: Text('Version Error'),
-            content: Text(
+            title: const Text('Version Error'),
+            content: const Text(
                 'Proxmox VE version not supported, please update your instance to use this app.'),
             actions: [
-              FlatButton(
+              TextButton(
                 onPressed: () => Navigator.of(context).pop(),
-                child: Text('Close'),
+                child: const Text('Close'),
               ),
             ],
           ),
@@ -472,70 +570,105 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
     } catch (e, trace) {
       print(e);
       print(trace);
-      if (e.runtimeType == HandshakeException) {
-        showDialog(
-          context: context,
-          builder: (context) => ProxmoxCertificateErrorDialog(),
-        );
+      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 host = _originController.text.trim();
-    var apiBaseUrl = Uri.https(host, '');
-
-    RegExp portRE = new RegExp(r":\d{1,5}$");
-
-    if (!portRE.hasMatch(host)) {
-      _originController.text += ':8006';
-      apiBaseUrl = apiBaseUrl.replace(port: 8006);
-    }
-
+  Future<List<PveAccessDomainModel?>?> _loadAccessDomains(Uri uri) async {
     final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
     List<PveAccessDomainModel?>? response;
     try {
-      response =
-          await proxclient.accessDomains(apiBaseUrl, settings.sslValidation!);
+      response = await proxclient.accessDomains(uri, settings.sslValidation!);
     } on proxclient.ProxmoxApiException catch (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));
 
@@ -545,7 +678,7 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
     );
 
     setState(() {
-      _progressModel.inProgress = false;
+      _progressModel.inProgress -= 1;
       _selectedDomain = selection;
     });
 
@@ -572,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(),
             )
           ],
@@ -595,6 +727,29 @@ 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;
 
@@ -606,14 +761,14 @@ class ProxmoxApiErrorDialog extends StatelessWidget {
   @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'),
         ),
       ],
     );
@@ -628,32 +783,43 @@ class ProxmoxCertificateErrorDialog extends StatelessWidget {
   @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, '');
+}