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';
102 DropdownButtonFormField(
103 decoration: InputDecoration(icon: Icon(Icons.domain)),
104 items: widget.accessDomains
105 .map((e) => DropdownMenuItem(
107 title: Text(e.realm),
108 subtitle: Text(e.comment ?? ''),
113 onChanged: widget.onDomainChanged,
114 selectedItemBuilder: (context) =>
115 widget.accessDomains.map((e) => Text(e.realm)).toList(),
116 value: widget.selectedDomain,
121 decoration: InputDecoration(
122 icon: Icon(Icons.lock),
123 labelText: 'Password',
125 controller: widget.passwordController,
126 obscureText: _obscure,
128 focusNode: passwordFocusNode,
131 return 'Please enter password';
135 onFieldSubmitted: (value) => widget.onPasswordSubmitted(),
138 alignment: Alignment.bottomRight,
140 constraints: BoxConstraints.tight(Size(58, 58)),
142 icon: Icon(_obscure ? Icons.visibility : Icons.visibility_off),
143 onPressed: () => setState(() {
144 _obscure = !_obscure;
156 passwordFocusNode?.dispose();
161 class ProxmoxLoginPage extends StatefulWidget {
162 final ProxmoxLoginModel userModel;
165 const ProxmoxLoginPage({
169 }) : super(key: key);
171 _ProxmoxLoginPageState createState() => _ProxmoxLoginPageState();
174 class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
175 final _originController = TextEditingController();
176 final _usernameController = TextEditingController();
177 final _passwordController = TextEditingController();
178 Future<List<PveAccessDomainModel>> _accessDomains;
179 PveAccessDomainModel _selectedDomain;
180 final _formKey = GlobalKey<FormState>();
181 ProxmoxProgressModel _progressModel;
182 bool _submittButtonEnabled = true;
186 final userModel = widget.userModel;
187 _progressModel = ProxmoxProgressModel();
188 if (!widget.isCreate && userModel != null) {
191 ..message = 'Connection test...';
192 _originController.text =
193 '${userModel.origin?.host}:${userModel.origin?.port}';
194 _accessDomains = _getAccessDomains();
195 _usernameController.text = userModel.username;
200 Widget build(BuildContext context) {
202 data: ThemeData.dark().copyWith(accentColor: Color(0xFFE47225)),
204 backgroundColor: Theme.of(context).primaryColor,
205 extendBodyBehindAppBar: true,
208 backgroundColor: Colors.transparent,
210 icon: Icon(Icons.close),
211 onPressed: () => Navigator.of(context).pop(),
216 SingleChildScrollView(
217 child: ConstrainedBox(
218 constraints: BoxConstraints.tightFor(
219 height: MediaQuery.of(context).size.height),
221 padding: const EdgeInsets.all(8.0),
222 child: FutureBuilder<List<PveAccessDomainModel>>(
223 future: _accessDomains,
224 builder: (context, snapshot) {
229 _submittButtonEnabled =
230 _formKey.currentState.validate();
234 mainAxisAlignment: MainAxisAlignment.center,
239 mainAxisAlignment: MainAxisAlignment.center,
244 fontFamily: 'Proxmox',
259 originController: _originController,
260 originValidator: (value) {
262 return 'Please enter origin';
264 if (value.startsWith('https://') ||
265 value.startsWith('http://')) {
266 return 'Do not prefix with scheme';
269 Uri.https(value, '');
271 } on FormatException catch (_) {
272 return 'Invalid URI';
275 usernameController: _usernameController,
276 passwordController: _passwordController,
277 accessDomains: snapshot.data,
278 selectedDomain: _selectedDomain,
279 onDomainChanged: (value) {
281 _selectedDomain = value;
284 onOriginSubmitted: _submittButtonEnabled
287 _formKey.currentState.validate();
289 _submittButtonEnabled = isValid;
299 onPasswordSubmitted: _submittButtonEnabled
302 _formKey.currentState.validate();
304 _submittButtonEnabled = isValid;
307 _onLoginButtonPressed();
312 if (snapshot.hasData)
315 alignment: Alignment.bottomCenter,
317 width: MediaQuery.of(context).size.width,
319 onPressed: _submittButtonEnabled
321 final isValid = _formKey
325 _submittButtonEnabled =
329 _onLoginButtonPressed();
333 color: Color(0xFFE47225),
334 disabledColor: Colors.grey,
335 child: Text('Continue'),
340 if (!snapshot.hasData)
343 alignment: Alignment.bottomCenter,
345 width: MediaQuery.of(context).size.width,
347 onPressed: _submittButtonEnabled
349 final isValid = _formKey
353 _submittButtonEnabled =
364 color: Color(0xFFE47225),
365 child: Text('Continue'),
366 disabledColor: Colors.grey,
378 if (_progressModel.inProgress)
379 ProxmoxProgressOverlay(message: _progressModel.message),
386 Future<void> _onLoginButtonPressed() async {
390 ..message = 'Authenticating...';
394 final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
396 //cleaned form fields
397 final origin = Uri.https(_originController.text.trim(), '');
398 final username = _usernameController.text.trim();
399 final password = _passwordController.text.trim();
400 final realm = _selectedDomain.realm;
402 var client = await proxclient.authenticate(
403 '$username@$realm', password, origin, settings.sslValidation);
405 if (client.credentials.tfa) {
406 client = await Navigator.of(context).push(MaterialPageRoute(
407 builder: (context) => ProxmoxTfaForm(
413 var loginStorage = await ProxmoxLoginStorage.fromLocalStorage();
415 if (widget.isCreate) {
416 final newLogin = ProxmoxLoginModel((b) => b
418 ..username = username
420 ..productType = ProxmoxProductType.pve
421 ..ticket = client.credentials.ticket);
423 loginStorage = loginStorage.rebuild((b) => b..logins.add(newLogin));
425 loginStorage = loginStorage.rebuild((b) => b
426 ..logins.remove(widget.userModel)
427 ..logins.add(widget.userModel
428 .rebuild((b) => b..ticket = client.credentials.ticket)));
430 await loginStorage.saveToDisk();
432 Navigator.of(context).pop(client);
433 } on proxclient.ProxmoxApiException catch (e) {
437 builder: (context) => ProxmoxApiErrorDialog(
444 if (e.runtimeType == HandshakeException) {
447 builder: (context) => ProxmoxCertificateErrorDialog(),
452 _progressModel.inProgress = false;
456 Future<List<PveAccessDomainModel>> _getAccessDomains() async {
460 ..message = 'Connection test...';
462 var apiBaseUrl = Uri.https(_originController.text.trim(), '');
464 if (!apiBaseUrl.hasPort) {
465 _originController.text += ':8006';
466 apiBaseUrl = apiBaseUrl.replace(port: 8006);
469 final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
470 List<PveAccessDomainModel> response;
473 await proxclient.accessDomains(apiBaseUrl, settings.sslValidation);
474 } on proxclient.ProxmoxApiException catch (e) {
478 builder: (context) => ProxmoxApiErrorDialog(
485 if (e.runtimeType == HandshakeException) {
488 builder: (context) => ProxmoxCertificateErrorDialog(),
493 builder: (context) => AlertDialog(
494 title: Text('Connection error'),
495 content: Text('Could not establish connection.'),
498 onPressed: () => Navigator.of(context).pop(),
499 child: Text('Close'),
507 response?.sort((a, b) => a.realm.compareTo(b.realm));
509 final selection = response?.singleWhere(
510 (e) => e.realm == widget.userModel?.realm,
511 orElse: () => response?.first,
515 _progressModel.inProgress = false;
516 _selectedDomain = selection;
524 _originController.dispose();
525 _usernameController.dispose();
526 _passwordController.dispose();
531 class ProxmoxProgressOverlay extends StatelessWidget {
532 const ProxmoxProgressOverlay({
534 @required this.message,
535 }) : super(key: key);
537 final String message;
540 Widget build(BuildContext context) {
542 decoration: new BoxDecoration(color: Colors.black.withOpacity(0.5)),
545 mainAxisAlignment: MainAxisAlignment.center,
550 color: Theme.of(context).accentColor,
555 padding: const EdgeInsets.only(top: 20.0),
556 child: CircularProgressIndicator(),
565 class ProxmoxApiErrorDialog extends StatelessWidget {
566 final proxclient.ProxmoxApiException exception;
568 const ProxmoxApiErrorDialog({
570 @required this.exception,
571 }) : super(key: key);
574 Widget build(BuildContext context) {
576 title: Text('API Error'),
577 content: SingleChildScrollView(
578 child: Text(exception.message),
582 onPressed: () => Navigator.of(context).pop(),
583 child: Text('Close'),
590 class ProxmoxCertificateErrorDialog extends StatelessWidget {
591 const ProxmoxCertificateErrorDialog({
593 }) : super(key: key);
596 Widget build(BuildContext context) {
598 title: Text('Certificate error'),
599 content: SingleChildScrollView(
601 crossAxisAlignment: CrossAxisAlignment.start,
603 Text('Your connection is not private.'),
605 'Note: Consider to disable SSL validation,'
606 ' if you use a self signed, not commonly trusted, certificate.',
607 style: Theme.of(context).textTheme.caption,
614 onPressed: () => Navigator.of(context).pop(),
615 child: Text('Close'),
618 onPressed: () => Navigator.of(context).pushReplacement(
620 builder: (context) => ProxmoxGeneralSettingsForm())),
621 child: Text('Settings'),