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(),
70 mainAxisAlignment: MainAxisAlignment.center,
73 decoration: InputDecoration(
74 icon: Icon(Icons.vpn_lock),
77 controller: widget.originController,
81 decoration: InputDecoration(
82 icon: Icon(Icons.person),
83 labelText: 'Username',
85 controller: widget.usernameController,
88 return 'Please enter username';
92 autofillHints: [AutofillHints.username],
94 DropdownButtonFormField(
95 decoration: InputDecoration(icon: Icon(Icons.domain)),
96 items: widget.accessDomains
97 .map((e) => DropdownMenuItem(
100 subtitle: Text(e.comment ?? ''),
105 onChanged: widget.onDomainChanged,
106 selectedItemBuilder: (context) =>
107 widget.accessDomains.map((e) => Text(e.realm)).toList(),
108 value: widget.selectedDomain,
113 decoration: InputDecoration(
114 icon: Icon(Icons.lock),
115 labelText: 'Password',
117 controller: widget.passwordController,
118 obscureText: _obscure,
120 focusNode: passwordFocusNode,
123 return 'Please enter password';
127 onFieldSubmitted: (value) => widget.onPasswordSubmitted(),
128 autofillHints: [AutofillHints.password],
131 alignment: Alignment.bottomRight,
133 constraints: BoxConstraints.tight(Size(58, 58)),
135 icon: Icon(_obscure ? Icons.visibility : Icons.visibility_off),
136 onPressed: () => setState(() {
137 _obscure = !_obscure;
149 passwordFocusNode?.dispose();
154 class ProxmoxLoginPage extends StatefulWidget {
155 final ProxmoxLoginModel userModel;
159 const ProxmoxLoginPage({
164 }) : super(key: key);
166 _ProxmoxLoginPageState createState() => _ProxmoxLoginPageState();
169 class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
170 final _originController = TextEditingController();
171 final _usernameController = TextEditingController();
172 final _passwordController = TextEditingController();
173 Future<List<PveAccessDomainModel>> _accessDomains;
174 PveAccessDomainModel _selectedDomain;
175 final _formKey = GlobalKey<FormState>();
176 ProxmoxProgressModel _progressModel;
177 bool _submittButtonEnabled = true;
182 final userModel = widget.userModel;
183 _progressModel = ProxmoxProgressModel();
184 if (!widget.isCreate && userModel != null) {
187 ..message = 'Connection test...';
188 _originController.text =
189 '${userModel.origin?.host}:${userModel.origin?.port}';
190 _accessDomains = _getAccessDomains();
191 _usernameController.text = userModel.username;
192 if (widget.ticket.isNotEmpty) {
193 _onLoginButtonPressed(ticket: widget.ticket, mRealm: userModel.realm);
199 Widget build(BuildContext context) {
201 data: ThemeData.dark().copyWith(accentColor: Color(0xFFE47225)),
203 backgroundColor: Theme.of(context).primaryColor,
204 extendBodyBehindAppBar: true,
207 backgroundColor: Colors.transparent,
209 icon: Icon(Icons.close),
210 onPressed: () => Navigator.of(context).pop(),
215 SingleChildScrollView(
216 child: ConstrainedBox(
217 constraints: BoxConstraints.tightFor(
218 height: MediaQuery.of(context).size.height),
220 padding: const EdgeInsets.all(8.0),
221 child: FutureBuilder<List<PveAccessDomainModel>>(
222 future: _accessDomains,
223 builder: (context, snapshot) {
228 _submittButtonEnabled =
229 _formKey.currentState.validate();
233 mainAxisAlignment: MainAxisAlignment.center,
238 mainAxisAlignment: MainAxisAlignment.center,
241 'assets/images/proxmox_logo_symbol_wordmark.png',
242 package: 'proxmox_login_manager',
249 originController: _originController,
250 originValidator: (value) {
252 return 'Please enter origin';
254 if (value.startsWith('https://') ||
255 value.startsWith('http://')) {
256 return 'Do not prefix with scheme';
259 Uri.https(value, '');
261 } on FormatException catch (_) {
262 return 'Invalid URI';
265 usernameController: _usernameController,
266 passwordController: _passwordController,
267 accessDomains: snapshot.data,
268 selectedDomain: _selectedDomain,
269 onDomainChanged: (value) {
271 _selectedDomain = value;
274 onOriginSubmitted: _submittButtonEnabled
277 _formKey.currentState.validate();
279 _submittButtonEnabled = isValid;
289 onPasswordSubmitted: _submittButtonEnabled
292 _formKey.currentState.validate();
294 _submittButtonEnabled = isValid;
297 _onLoginButtonPressed();
302 if (snapshot.hasData)
305 alignment: Alignment.bottomCenter,
307 width: MediaQuery.of(context).size.width,
309 onPressed: _submittButtonEnabled
311 final isValid = _formKey
315 _submittButtonEnabled =
319 _onLoginButtonPressed();
323 color: Color(0xFFE47225),
324 disabledColor: Colors.grey,
325 child: Text('Continue'),
330 if (!snapshot.hasData)
333 alignment: Alignment.bottomCenter,
335 width: MediaQuery.of(context).size.width,
337 onPressed: _submittButtonEnabled
339 final isValid = _formKey
343 _submittButtonEnabled =
354 color: Color(0xFFE47225),
355 child: Text('Continue'),
356 disabledColor: Colors.grey,
368 if (_progressModel.inProgress)
369 ProxmoxProgressOverlay(message: _progressModel.message),
376 Future<void> _onLoginButtonPressed(
377 {String ticket = '', String mRealm}) async {
381 ..message = 'Authenticating...';
385 final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
387 //cleaned form fields
388 final origin = Uri.https(_originController.text.trim(), '');
389 final username = _usernameController.text.trim();
391 ticket.isNotEmpty ? ticket : _passwordController.text.trim();
392 final realm = _selectedDomain?.realm ?? mRealm;
394 var client = await proxclient.authenticate(
395 '$username@$realm', password, origin, settings.sslValidation);
397 if (client.credentials.tfa) {
398 client = await Navigator.of(context).push(MaterialPageRoute(
399 builder: (context) => ProxmoxTfaForm(
405 final status = await client.getClusterStatus();
407 status.singleWhere((element) => element.local ?? false).name;
408 var loginStorage = await ProxmoxLoginStorage.fromLocalStorage();
410 if (widget.isCreate) {
411 final newLogin = ProxmoxLoginModel((b) => b
413 ..username = username
415 ..productType = ProxmoxProductType.pve
416 ..ticket = client.credentials.ticket
417 ..hostname = hostname);
419 loginStorage = loginStorage.rebuild((b) => b..logins.add(newLogin));
421 loginStorage = loginStorage.rebuild((b) => b
422 ..logins.rebuildWhere(
423 (m) => m == widget.userModel,
425 ..ticket = client.credentials.ticket
426 ..hostname = hostname));
428 await loginStorage.saveToDisk();
430 Navigator.of(context).pop(client);
431 } on proxclient.ProxmoxApiException catch (e) {
435 builder: (context) => ProxmoxApiErrorDialog(
442 if (e.runtimeType == HandshakeException) {
445 builder: (context) => ProxmoxCertificateErrorDialog(),
450 _progressModel.inProgress = false;
454 Future<List<PveAccessDomainModel>> _getAccessDomains() async {
458 ..message = 'Connection test...';
460 var apiBaseUrl = Uri.https(_originController.text.trim(), '');
462 if (!apiBaseUrl.hasPort) {
463 _originController.text += ':8006';
464 apiBaseUrl = apiBaseUrl.replace(port: 8006);
467 final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
468 List<PveAccessDomainModel> response;
471 await proxclient.accessDomains(apiBaseUrl, settings.sslValidation);
472 } on proxclient.ProxmoxApiException catch (e) {
476 builder: (context) => ProxmoxApiErrorDialog(
483 if (e.runtimeType == HandshakeException) {
486 builder: (context) => ProxmoxCertificateErrorDialog(),
491 builder: (context) => AlertDialog(
492 title: Text('Connection error'),
493 content: Text('Could not establish connection.'),
496 onPressed: () => Navigator.of(context).pop(),
497 child: Text('Close'),
505 response?.sort((a, b) => a.realm.compareTo(b.realm));
507 final selection = response?.singleWhere(
508 (e) => e.realm == widget.userModel?.realm,
509 orElse: () => response?.first,
513 _progressModel.inProgress = false;
514 _selectedDomain = selection;
522 _originController.dispose();
523 _usernameController.dispose();
524 _passwordController.dispose();
529 class ProxmoxProgressOverlay extends StatelessWidget {
530 const ProxmoxProgressOverlay({
532 @required this.message,
533 }) : super(key: key);
535 final String message;
538 Widget build(BuildContext context) {
540 decoration: new BoxDecoration(color: Colors.black.withOpacity(0.5)),
543 mainAxisAlignment: MainAxisAlignment.center,
548 color: Theme.of(context).accentColor,
553 padding: const EdgeInsets.only(top: 20.0),
554 child: CircularProgressIndicator(),
563 class ProxmoxApiErrorDialog extends StatelessWidget {
564 final proxclient.ProxmoxApiException exception;
566 const ProxmoxApiErrorDialog({
568 @required this.exception,
569 }) : super(key: key);
572 Widget build(BuildContext context) {
574 title: Text('API Error'),
575 content: SingleChildScrollView(
576 child: Text(exception.message),
580 onPressed: () => Navigator.of(context).pop(),
581 child: Text('Close'),
588 class ProxmoxCertificateErrorDialog extends StatelessWidget {
589 const ProxmoxCertificateErrorDialog({
591 }) : super(key: key);
594 Widget build(BuildContext context) {
596 title: Text('Certificate error'),
597 content: SingleChildScrollView(
599 crossAxisAlignment: CrossAxisAlignment.start,
601 Text('Your connection is not private.'),
603 'Note: Consider to disable SSL validation,'
604 ' if you use a self signed, not commonly trusted, certificate.',
605 style: Theme.of(context).textTheme.caption,
612 onPressed: () => Navigator.of(context).pop(),
613 child: Text('Close'),
616 onPressed: () => Navigator.of(context).pushReplacement(
618 builder: (context) => ProxmoxGeneralSettingsForm())),
619 child: Text('Settings'),