3 import 'package:flutter/material.dart';
4 import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart'
6 import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart';
7 import 'package:proxmox_login_manager/proxmox_general_settings_form.dart';
8 import 'package:proxmox_login_manager/proxmox_general_settings_model.dart';
9 import 'package:proxmox_login_manager/proxmox_login_model.dart';
10 import 'package:proxmox_login_manager/proxmox_tfa_form.dart';
12 class ProxmoxProgressModel {
15 ProxmoxProgressModel({
16 this.inProgress = false,
17 this.message = 'Loading...',
21 class ProxmoxLoginForm extends StatefulWidget {
22 final TextEditingController originController;
23 final FormFieldValidator<String> originValidator;
24 final TextEditingController usernameController;
25 final TextEditingController passwordController;
26 final List<PveAccessDomainModel> accessDomains;
27 final PveAccessDomainModel selectedDomain;
28 final ValueChanged<PveAccessDomainModel> onDomainChanged;
29 final Function onPasswordSubmitted;
30 final Function onOriginSubmitted;
32 const ProxmoxLoginForm({
34 @required this.originController,
35 @required this.usernameController,
36 @required this.passwordController,
37 @required this.accessDomains,
38 @required this.originValidator,
40 @required this.onDomainChanged,
41 this.onPasswordSubmitted,
42 this.onOriginSubmitted,
46 _ProxmoxLoginFormState createState() => _ProxmoxLoginFormState();
49 class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> {
51 FocusNode passwordFocusNode;
57 if (widget.usernameController.text.isNotEmpty) {
58 passwordFocusNode = FocusNode();
59 passwordFocusNode.requestFocus();
64 Widget build(BuildContext context) {
65 if (widget.accessDomains == null) {
67 decoration: InputDecoration(
68 icon: Icon(Icons.vpn_lock),
70 hintText: 'e.g. 192.168.1.2',
71 helperText: 'Protocol (https) and default port (8006) implied'),
72 controller: widget.originController,
73 validator: widget.originValidator,
74 onFieldSubmitted: (value) => widget.onOriginSubmitted(),
79 mainAxisAlignment: MainAxisAlignment.center,
82 decoration: InputDecoration(
83 icon: Icon(Icons.vpn_lock),
86 controller: widget.originController,
90 decoration: InputDecoration(
91 icon: Icon(Icons.person),
92 labelText: 'Username',
94 controller: widget.usernameController,
97 return 'Please enter username';
101 autofillHints: [AutofillHints.username],
103 DropdownButtonFormField(
104 decoration: InputDecoration(icon: Icon(Icons.domain)),
105 items: widget.accessDomains
106 .map((e) => DropdownMenuItem(
108 title: Text(e.realm),
109 subtitle: Text(e.comment ?? ''),
114 onChanged: widget.onDomainChanged,
115 selectedItemBuilder: (context) =>
116 widget.accessDomains.map((e) => Text(e.realm)).toList(),
117 value: widget.selectedDomain,
122 decoration: InputDecoration(
123 icon: Icon(Icons.lock),
124 labelText: 'Password',
126 controller: widget.passwordController,
127 obscureText: _obscure,
129 focusNode: passwordFocusNode,
132 return 'Please enter password';
136 onFieldSubmitted: (value) => widget.onPasswordSubmitted(),
137 autofillHints: [AutofillHints.password],
140 alignment: Alignment.bottomRight,
142 constraints: BoxConstraints.tight(Size(58, 58)),
144 icon: Icon(_obscure ? Icons.visibility : Icons.visibility_off),
145 onPressed: () => setState(() {
146 _obscure = !_obscure;
158 passwordFocusNode?.dispose();
163 class ProxmoxLoginPage extends StatefulWidget {
164 final ProxmoxLoginModel userModel;
167 const ProxmoxLoginPage({
171 }) : super(key: key);
173 _ProxmoxLoginPageState createState() => _ProxmoxLoginPageState();
176 class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
177 final _originController = TextEditingController();
178 final _usernameController = TextEditingController();
179 final _passwordController = TextEditingController();
180 Future<List<PveAccessDomainModel>> _accessDomains;
181 PveAccessDomainModel _selectedDomain;
182 final _formKey = GlobalKey<FormState>();
183 ProxmoxProgressModel _progressModel;
184 bool _submittButtonEnabled = true;
188 final userModel = widget.userModel;
189 _progressModel = ProxmoxProgressModel();
190 if (!widget.isCreate && userModel != null) {
193 ..message = 'Connection test...';
194 _originController.text =
195 '${userModel.origin?.host}:${userModel.origin?.port}';
196 _accessDomains = _getAccessDomains();
197 _usernameController.text = userModel.username;
202 Widget build(BuildContext context) {
204 data: ThemeData.dark().copyWith(accentColor: Color(0xFFE47225)),
206 backgroundColor: Theme.of(context).primaryColor,
207 extendBodyBehindAppBar: true,
210 backgroundColor: Colors.transparent,
212 icon: Icon(Icons.close),
213 onPressed: () => Navigator.of(context).pop(),
218 SingleChildScrollView(
219 child: ConstrainedBox(
220 constraints: BoxConstraints.tightFor(
221 height: MediaQuery.of(context).size.height),
223 padding: const EdgeInsets.all(8.0),
224 child: FutureBuilder<List<PveAccessDomainModel>>(
225 future: _accessDomains,
226 builder: (context, snapshot) {
231 _submittButtonEnabled =
232 _formKey.currentState.validate();
236 mainAxisAlignment: MainAxisAlignment.center,
241 mainAxisAlignment: MainAxisAlignment.center,
246 fontFamily: 'Proxmox',
261 originController: _originController,
262 originValidator: (value) {
264 return 'Please enter origin';
266 if (value.startsWith('https://') ||
267 value.startsWith('http://')) {
268 return 'Do not prefix with scheme';
271 Uri.https(value, '');
273 } on FormatException catch (_) {
274 return 'Invalid URI';
277 usernameController: _usernameController,
278 passwordController: _passwordController,
279 accessDomains: snapshot.data,
280 selectedDomain: _selectedDomain,
281 onDomainChanged: (value) {
283 _selectedDomain = value;
286 onOriginSubmitted: _submittButtonEnabled
289 _formKey.currentState.validate();
291 _submittButtonEnabled = isValid;
301 onPasswordSubmitted: _submittButtonEnabled
304 _formKey.currentState.validate();
306 _submittButtonEnabled = isValid;
309 _onLoginButtonPressed();
314 if (snapshot.hasData)
317 alignment: Alignment.bottomCenter,
319 width: MediaQuery.of(context).size.width,
321 onPressed: _submittButtonEnabled
323 final isValid = _formKey
327 _submittButtonEnabled =
331 _onLoginButtonPressed();
335 color: Color(0xFFE47225),
336 disabledColor: Colors.grey,
337 child: Text('Continue'),
342 if (!snapshot.hasData)
345 alignment: Alignment.bottomCenter,
347 width: MediaQuery.of(context).size.width,
349 onPressed: _submittButtonEnabled
351 final isValid = _formKey
355 _submittButtonEnabled =
366 color: Color(0xFFE47225),
367 child: Text('Continue'),
368 disabledColor: Colors.grey,
380 if (_progressModel.inProgress)
381 ProxmoxProgressOverlay(message: _progressModel.message),
388 Future<void> _onLoginButtonPressed() async {
392 ..message = 'Authenticating...';
396 final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
398 //cleaned form fields
399 final origin = Uri.https(_originController.text.trim(), '');
400 final username = _usernameController.text.trim();
401 final password = _passwordController.text.trim();
402 final realm = _selectedDomain.realm;
404 var client = await proxclient.authenticate(
405 '$username@$realm', password, origin, settings.sslValidation);
407 if (client.credentials.tfa) {
408 client = await Navigator.of(context).push(MaterialPageRoute(
409 builder: (context) => ProxmoxTfaForm(
415 var loginStorage = await ProxmoxLoginStorage.fromLocalStorage();
417 if (widget.isCreate) {
418 final newLogin = ProxmoxLoginModel((b) => b
420 ..username = username
422 ..productType = ProxmoxProductType.pve
423 ..ticket = client.credentials.ticket);
425 loginStorage = loginStorage.rebuild((b) => b..logins.add(newLogin));
427 loginStorage = loginStorage.rebuild((b) => b
428 ..logins.remove(widget.userModel)
429 ..logins.add(widget.userModel
430 .rebuild((b) => b..ticket = client.credentials.ticket)));
432 await loginStorage.saveToDisk();
434 Navigator.of(context).pop(client);
435 } on proxclient.ProxmoxApiException catch (e) {
439 builder: (context) => ProxmoxApiErrorDialog(
446 if (e.runtimeType == HandshakeException) {
449 builder: (context) => ProxmoxCertificateErrorDialog(),
454 _progressModel.inProgress = false;
458 Future<List<PveAccessDomainModel>> _getAccessDomains() async {
462 ..message = 'Connection test...';
464 var apiBaseUrl = Uri.https(_originController.text.trim(), '');
466 if (!apiBaseUrl.hasPort) {
467 _originController.text += ':8006';
468 apiBaseUrl = apiBaseUrl.replace(port: 8006);
471 final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
472 List<PveAccessDomainModel> response;
475 await proxclient.accessDomains(apiBaseUrl, settings.sslValidation);
476 } on proxclient.ProxmoxApiException catch (e) {
480 builder: (context) => ProxmoxApiErrorDialog(
487 if (e.runtimeType == HandshakeException) {
490 builder: (context) => ProxmoxCertificateErrorDialog(),
495 builder: (context) => AlertDialog(
496 title: Text('Connection error'),
497 content: Text('Could not establish connection.'),
500 onPressed: () => Navigator.of(context).pop(),
501 child: Text('Close'),
509 response?.sort((a, b) => a.realm.compareTo(b.realm));
511 final selection = response?.singleWhere(
512 (e) => e.realm == widget.userModel?.realm,
513 orElse: () => response?.first,
517 _progressModel.inProgress = false;
518 _selectedDomain = selection;
526 _originController.dispose();
527 _usernameController.dispose();
528 _passwordController.dispose();
533 class ProxmoxProgressOverlay extends StatelessWidget {
534 const ProxmoxProgressOverlay({
536 @required this.message,
537 }) : super(key: key);
539 final String message;
542 Widget build(BuildContext context) {
544 decoration: new BoxDecoration(color: Colors.black.withOpacity(0.5)),
547 mainAxisAlignment: MainAxisAlignment.center,
552 color: Theme.of(context).accentColor,
557 padding: const EdgeInsets.only(top: 20.0),
558 child: CircularProgressIndicator(),
567 class ProxmoxApiErrorDialog extends StatelessWidget {
568 final proxclient.ProxmoxApiException exception;
570 const ProxmoxApiErrorDialog({
572 @required this.exception,
573 }) : super(key: key);
576 Widget build(BuildContext context) {
578 title: Text('API Error'),
579 content: SingleChildScrollView(
580 child: Text(exception.message),
584 onPressed: () => Navigator.of(context).pop(),
585 child: Text('Close'),
592 class ProxmoxCertificateErrorDialog extends StatelessWidget {
593 const ProxmoxCertificateErrorDialog({
595 }) : super(key: key);
598 Widget build(BuildContext context) {
600 title: Text('Certificate error'),
601 content: SingleChildScrollView(
603 crossAxisAlignment: CrossAxisAlignment.start,
605 Text('Your connection is not private.'),
607 'Note: Consider to disable SSL validation,'
608 ' if you use a self signed, not commonly trusted, certificate.',
609 style: Theme.of(context).textTheme.caption,
616 onPressed: () => Navigator.of(context).pop(),
617 child: Text('Close'),
620 onPressed: () => Navigator.of(context).pushReplacement(
622 builder: (context) => ProxmoxGeneralSettingsForm())),
623 child: Text('Settings'),