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';
11 import 'package:proxmox_login_manager/extension.dart';
13 class ProxmoxProgressModel {
16 ProxmoxProgressModel({
17 this.inProgress = false,
18 this.message = 'Loading...',
22 class ProxmoxLoginForm extends StatefulWidget {
23 final TextEditingController originController;
24 final FormFieldValidator<String> originValidator;
25 final TextEditingController usernameController;
26 final TextEditingController passwordController;
27 final List<PveAccessDomainModel> accessDomains;
28 final PveAccessDomainModel selectedDomain;
29 final ValueChanged<PveAccessDomainModel> onDomainChanged;
30 final Function onPasswordSubmitted;
31 final Function onOriginSubmitted;
33 const ProxmoxLoginForm({
35 @required this.originController,
36 @required this.usernameController,
37 @required this.passwordController,
38 @required this.accessDomains,
39 @required this.originValidator,
41 @required this.onDomainChanged,
42 this.onPasswordSubmitted,
43 this.onOriginSubmitted,
47 _ProxmoxLoginFormState createState() => _ProxmoxLoginFormState();
50 class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> {
52 FocusNode passwordFocusNode;
55 Widget build(BuildContext context) {
56 if (widget.accessDomains == null) {
58 decoration: InputDecoration(
59 icon: Icon(Icons.vpn_lock),
61 hintText: 'e.g. 192.168.1.2',
62 helperText: 'Protocol (https) and default port (8006) implied'),
63 controller: widget.originController,
64 validator: widget.originValidator,
65 onFieldSubmitted: (value) => widget.onOriginSubmitted(),
71 mainAxisAlignment: MainAxisAlignment.center,
74 decoration: InputDecoration(
75 icon: Icon(Icons.vpn_lock),
78 controller: widget.originController,
82 decoration: InputDecoration(
83 icon: Icon(Icons.person),
84 labelText: 'Username',
86 controller: widget.usernameController,
89 return 'Please enter username';
93 autofillHints: [AutofillHints.username],
95 DropdownButtonFormField(
96 decoration: InputDecoration(icon: Icon(Icons.domain)),
97 items: widget.accessDomains
98 .map((e) => DropdownMenuItem(
100 title: Text(e.realm),
101 subtitle: Text(e.comment ?? ''),
106 onChanged: widget.onDomainChanged,
107 selectedItemBuilder: (context) =>
108 widget.accessDomains.map((e) => Text(e.realm)).toList(),
109 value: widget.selectedDomain,
114 decoration: InputDecoration(
115 icon: Icon(Icons.lock),
116 labelText: 'Password',
118 controller: widget.passwordController,
119 obscureText: _obscure,
121 focusNode: passwordFocusNode,
124 return 'Please enter password';
128 onFieldSubmitted: (value) => widget.onPasswordSubmitted(),
129 autofillHints: [AutofillHints.password],
132 alignment: Alignment.bottomRight,
134 constraints: BoxConstraints.tight(Size(58, 58)),
137 Icon(_obscure ? Icons.visibility : Icons.visibility_off),
138 onPressed: () => setState(() {
139 _obscure = !_obscure;
152 passwordFocusNode?.dispose();
157 class ProxmoxLoginPage extends StatefulWidget {
158 final ProxmoxLoginModel userModel;
162 const ProxmoxLoginPage({
167 }) : super(key: key);
169 _ProxmoxLoginPageState createState() => _ProxmoxLoginPageState();
172 class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
173 final _originController = TextEditingController();
174 final _usernameController = TextEditingController();
175 final _passwordController = TextEditingController();
176 Future<List<PveAccessDomainModel>> _accessDomains;
177 PveAccessDomainModel _selectedDomain;
178 final _formKey = GlobalKey<FormState>();
179 ProxmoxProgressModel _progressModel;
180 bool _submittButtonEnabled = true;
185 final userModel = widget.userModel;
186 _progressModel = ProxmoxProgressModel();
187 if (!widget.isCreate && userModel != null) {
190 ..message = 'Connection test...';
191 _originController.text =
192 '${userModel.origin?.host}:${userModel.origin?.port}';
193 _accessDomains = _getAccessDomains();
194 _usernameController.text = userModel.username;
195 if (widget.ticket.isNotEmpty && userModel.activeSession) {
196 _onLoginButtonPressed(ticket: widget.ticket, mRealm: userModel.realm);
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,
244 'assets/images/proxmox_logo_symbol_wordmark.png',
245 package: 'proxmox_login_manager',
252 originController: _originController,
253 originValidator: (value) {
255 return 'Please enter origin';
257 if (value.startsWith('https://') ||
258 value.startsWith('http://')) {
259 return 'Do not prefix with scheme';
262 Uri.https(value, '');
264 } on FormatException catch (_) {
265 return 'Invalid URI';
268 usernameController: _usernameController,
269 passwordController: _passwordController,
270 accessDomains: snapshot.data,
271 selectedDomain: _selectedDomain,
272 onDomainChanged: (value) {
274 _selectedDomain = value;
277 onOriginSubmitted: _submittButtonEnabled
280 _formKey.currentState.validate();
282 _submittButtonEnabled = isValid;
292 onPasswordSubmitted: _submittButtonEnabled
295 _formKey.currentState.validate();
297 _submittButtonEnabled = isValid;
300 _onLoginButtonPressed();
305 if (snapshot.hasData)
308 alignment: Alignment.bottomCenter,
310 width: MediaQuery.of(context).size.width,
312 onPressed: _submittButtonEnabled
314 final isValid = _formKey
318 _submittButtonEnabled =
322 _onLoginButtonPressed();
326 color: Color(0xFFE47225),
327 disabledColor: Colors.grey,
328 child: Text('Continue'),
333 if (!snapshot.hasData)
336 alignment: Alignment.bottomCenter,
338 width: MediaQuery.of(context).size.width,
340 onPressed: _submittButtonEnabled
342 final isValid = _formKey
346 _submittButtonEnabled =
357 color: Color(0xFFE47225),
358 child: Text('Continue'),
359 disabledColor: Colors.grey,
371 if (_progressModel.inProgress)
372 ProxmoxProgressOverlay(message: _progressModel.message),
379 Future<void> _onLoginButtonPressed(
380 {String ticket = '', String mRealm}) async {
384 ..message = 'Authenticating...';
388 final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
390 //cleaned form fields
391 final origin = Uri.https(_originController.text.trim(), '');
392 final username = _usernameController.text.trim();
394 ticket.isNotEmpty ? ticket : _passwordController.text.trim();
395 final realm = _selectedDomain?.realm ?? mRealm;
397 var client = await proxclient.authenticate(
398 '$username@$realm', password, origin, settings.sslValidation);
400 if (client.credentials.tfa) {
401 client = await Navigator.of(context).push(MaterialPageRoute(
402 builder: (context) => ProxmoxTfaForm(
407 if (client == null) {
409 _progressModel.inProgress = false;
415 final status = await client.getClusterStatus();
417 status.singleWhere((element) => element.local ?? false).name;
418 var loginStorage = await ProxmoxLoginStorage.fromLocalStorage();
420 if (widget.isCreate) {
421 final newLogin = ProxmoxLoginModel((b) => b
423 ..username = username
425 ..productType = ProxmoxProductType.pve
426 ..ticket = client.credentials.ticket
427 ..hostname = hostname);
429 loginStorage = loginStorage.rebuild((b) => b..logins.add(newLogin));
431 loginStorage = loginStorage.rebuild((b) => b
432 ..logins.rebuildWhere(
433 (m) => m == widget.userModel,
435 ..ticket = client.credentials.ticket
436 ..hostname = hostname));
438 await loginStorage.saveToDisk();
440 Navigator.of(context).pop(client);
441 } on proxclient.ProxmoxApiException catch (e) {
445 builder: (context) => ProxmoxApiErrorDialog(
452 if (e.runtimeType == HandshakeException) {
455 builder: (context) => ProxmoxCertificateErrorDialog(),
460 _progressModel.inProgress = false;
464 Future<List<PveAccessDomainModel>> _getAccessDomains() async {
468 ..message = 'Connection test...';
470 var apiBaseUrl = Uri.https(_originController.text.trim(), '');
472 if (!apiBaseUrl.hasPort) {
473 _originController.text += ':8006';
474 apiBaseUrl = apiBaseUrl.replace(port: 8006);
477 final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
478 List<PveAccessDomainModel> response;
481 await proxclient.accessDomains(apiBaseUrl, settings.sslValidation);
482 } on proxclient.ProxmoxApiException catch (e) {
486 builder: (context) => ProxmoxApiErrorDialog(
493 if (e.runtimeType == HandshakeException) {
496 builder: (context) => ProxmoxCertificateErrorDialog(),
501 builder: (context) => AlertDialog(
502 title: Text('Connection error'),
503 content: Text('Could not establish connection.'),
506 onPressed: () => Navigator.of(context).pop(),
507 child: Text('Close'),
515 response?.sort((a, b) => a.realm.compareTo(b.realm));
517 final selection = response?.singleWhere(
518 (e) => e.realm == widget.userModel?.realm,
519 orElse: () => response?.first,
523 _progressModel.inProgress = false;
524 _selectedDomain = selection;
532 _originController.dispose();
533 _usernameController.dispose();
534 _passwordController.dispose();
539 class ProxmoxProgressOverlay extends StatelessWidget {
540 const ProxmoxProgressOverlay({
542 @required this.message,
543 }) : super(key: key);
545 final String message;
548 Widget build(BuildContext context) {
550 decoration: new BoxDecoration(color: Colors.black.withOpacity(0.5)),
553 mainAxisAlignment: MainAxisAlignment.center,
558 color: Theme.of(context).accentColor,
563 padding: const EdgeInsets.only(top: 20.0),
564 child: CircularProgressIndicator(),
573 class ProxmoxApiErrorDialog extends StatelessWidget {
574 final proxclient.ProxmoxApiException exception;
576 const ProxmoxApiErrorDialog({
578 @required this.exception,
579 }) : super(key: key);
582 Widget build(BuildContext context) {
584 title: Text('API Error'),
585 content: SingleChildScrollView(
586 child: Text(exception.message),
590 onPressed: () => Navigator.of(context).pop(),
591 child: Text('Close'),
598 class ProxmoxCertificateErrorDialog extends StatelessWidget {
599 const ProxmoxCertificateErrorDialog({
601 }) : super(key: key);
604 Widget build(BuildContext context) {
606 title: Text('Certificate error'),
607 content: SingleChildScrollView(
609 crossAxisAlignment: CrossAxisAlignment.start,
611 Text('Your connection is not private.'),
613 'Note: Consider to disable SSL validation,'
614 ' if you use a self signed, not commonly trusted, certificate.',
615 style: Theme.of(context).textTheme.caption,
622 onPressed: () => Navigator.of(context).pop(),
623 child: Text('Close'),
626 onPressed: () => Navigator.of(context).pushReplacement(
628 builder: (context) => ProxmoxGeneralSettingsForm())),
629 child: Text('Settings'),