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';
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;
- }),
- ),
)
- ],
- ),
- ],
+ ],
+ ),
);
}
}
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),
],
),
);
}
- 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;
}
class ProxmoxProgressOverlay extends StatelessWidget {
const ProxmoxProgressOverlay({
- Key key,
- @required this.message,
+ Key? key,
+ required this.message,
}) : super(key: key);
final String message;
@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(),
)
],
}
}
+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'),
),
],
);
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, '');
+}