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,
207 SingleChildScrollView(
208 child: ConstrainedBox(
209 constraints: BoxConstraints.tightFor(
210 height: MediaQuery.of(context).size.height),
212 padding: const EdgeInsets.all(8.0),
213 child: FutureBuilder<List<PveAccessDomainModel>>(
214 future: _accessDomains,
215 builder: (context, snapshot) {
220 _submittButtonEnabled =
221 _formKey.currentState.validate();
225 mainAxisAlignment: MainAxisAlignment.center,
230 mainAxisAlignment: MainAxisAlignment.center,
235 fontFamily: 'Proxmox',
250 originController: _originController,
251 originValidator: (value) {
253 return 'Please enter origin';
255 if (value.startsWith('https://') ||
256 value.startsWith('http://')) {
257 return 'Do not prefix with scheme';
260 Uri.https(value, '');
262 } on FormatException catch (_) {
263 return 'Invalid URI';
266 usernameController: _usernameController,
267 passwordController: _passwordController,
268 accessDomains: snapshot.data,
269 selectedDomain: _selectedDomain,
270 onDomainChanged: (value) {
272 _selectedDomain = value;
275 onOriginSubmitted: _submittButtonEnabled
278 _formKey.currentState.validate();
280 _submittButtonEnabled = isValid;
290 onPasswordSubmitted: _submittButtonEnabled
293 _formKey.currentState.validate();
295 _submittButtonEnabled = isValid;
298 _onLoginButtonPressed();
303 if (snapshot.hasData)
306 alignment: Alignment.bottomCenter,
308 width: MediaQuery.of(context).size.width,
310 onPressed: _submittButtonEnabled
312 final isValid = _formKey
316 _submittButtonEnabled =
320 _onLoginButtonPressed();
324 color: Color(0xFFE47225),
325 disabledColor: Colors.grey,
326 child: Text('Continue'),
331 if (!snapshot.hasData)
334 alignment: Alignment.bottomCenter,
336 width: MediaQuery.of(context).size.width,
338 onPressed: _submittButtonEnabled
340 final isValid = _formKey
344 _submittButtonEnabled =
355 color: Color(0xFFE47225),
356 child: Text('Continue'),
357 disabledColor: Colors.grey,
369 if (_progressModel.inProgress)
370 ProxmoxProgressOverlay(message: _progressModel.message),
377 Future<void> _onLoginButtonPressed() 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();
390 final password = _passwordController.text.trim();
391 final realm = _selectedDomain.realm;
393 var client = await proxclient.authenticate(
394 '$username@$realm', password, origin, settings.sslValidation);
396 if (client.credentials.tfa) {
397 client = await Navigator.of(context).push(MaterialPageRoute(
398 builder: (context) => ProxmoxTfaForm(
404 var loginStorage = await ProxmoxLoginStorage.fromLocalStorage();
406 if (widget.isCreate) {
407 final newLogin = ProxmoxLoginModel((b) => b
409 ..username = username
411 ..productType = ProxmoxProductType.pve
412 ..ticket = client.credentials.ticket);
414 loginStorage = loginStorage.rebuild((b) => b..logins.add(newLogin));
416 loginStorage = loginStorage.rebuild((b) => b
417 ..logins.remove(widget.userModel)
418 ..logins.add(widget.userModel
419 .rebuild((b) => b..ticket = client.credentials.ticket)));
421 await loginStorage.saveToDisk();
423 Navigator.of(context).pop(client);
424 } on proxclient.ProxmoxApiException catch (e) {
428 builder: (context) => ProxmoxApiErrorDialog(
435 if (e.runtimeType == HandshakeException) {
438 builder: (context) => ProxmoxCertificateErrorDialog(),
443 _progressModel.inProgress = false;
447 Future<List<PveAccessDomainModel>> _getAccessDomains() async {
451 ..message = 'Connection test...';
453 var apiBaseUrl = Uri.https(_originController.text.trim(), '');
455 if (!apiBaseUrl.hasPort) {
456 _originController.text += ':8006';
457 apiBaseUrl = apiBaseUrl.replace(port: 8006);
460 final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
461 List<PveAccessDomainModel> response;
464 await proxclient.accessDomains(apiBaseUrl, settings.sslValidation);
465 } on proxclient.ProxmoxApiException catch (e) {
469 builder: (context) => ProxmoxApiErrorDialog(
476 if (e.runtimeType == HandshakeException) {
479 builder: (context) => ProxmoxCertificateErrorDialog(),
484 builder: (context) => AlertDialog(
485 title: Text('Connection error'),
486 content: Text('Could not establish connection.'),
489 onPressed: () => Navigator.of(context).pop(),
490 child: Text('Close'),
498 response?.sort((a, b) => a.realm.compareTo(b.realm));
500 final selection = response?.singleWhere(
501 (e) => e.realm == widget.userModel?.realm,
502 orElse: () => response?.first,
506 _progressModel.inProgress = false;
507 _selectedDomain = selection;
515 _originController.dispose();
516 _usernameController.dispose();
517 _passwordController.dispose();
522 class ProxmoxProgressOverlay extends StatelessWidget {
523 const ProxmoxProgressOverlay({
525 @required this.message,
526 }) : super(key: key);
528 final String message;
531 Widget build(BuildContext context) {
533 decoration: new BoxDecoration(color: Colors.black.withOpacity(0.5)),
536 mainAxisAlignment: MainAxisAlignment.center,
541 color: Theme.of(context).accentColor,
546 padding: const EdgeInsets.only(top: 20.0),
547 child: CircularProgressIndicator(),
556 class ProxmoxApiErrorDialog extends StatelessWidget {
557 final proxclient.ProxmoxApiException exception;
559 const ProxmoxApiErrorDialog({
561 @required this.exception,
562 }) : super(key: key);
565 Widget build(BuildContext context) {
567 title: Text('API Error'),
568 content: SingleChildScrollView(
569 child: Text(exception.message),
573 onPressed: () => Navigator.of(context).pop(),
574 child: Text('Close'),
581 class ProxmoxCertificateErrorDialog extends StatelessWidget {
582 const ProxmoxCertificateErrorDialog({
584 }) : super(key: key);
587 Widget build(BuildContext context) {
589 title: Text('Certificate error'),
590 content: SingleChildScrollView(
592 crossAxisAlignment: CrossAxisAlignment.start,
594 Text('Your connection is not private.'),
596 'Note: Consider to disable SSL validation,'
597 ' if you use a self signed, not commonly trusted, certificate.',
598 style: Theme.of(context).textTheme.caption,
605 onPressed: () => Navigator.of(context).pop(),
606 child: Text('Close'),
609 onPressed: () => Navigator.of(context).pushReplacement(
611 builder: (context) => ProxmoxGeneralSettingsForm())),
612 child: Text('Settings'),