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;
30 const ProxmoxLoginForm({
32 @required this.originController,
33 @required this.usernameController,
34 @required this.passwordController,
35 @required this.accessDomains,
36 @required this.originValidator,
38 @required this.onDomainChanged,
42 _ProxmoxLoginFormState createState() => _ProxmoxLoginFormState();
45 class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> {
47 FocusNode passwordFocusNode;
53 if (widget.usernameController.text.isNotEmpty) {
54 passwordFocusNode = FocusNode();
55 passwordFocusNode.requestFocus();
60 Widget build(BuildContext context) {
61 if (widget.accessDomains == null) {
63 decoration: InputDecoration(
64 icon: Icon(Icons.vpn_lock),
66 hintText: 'e.g. 192.168.1.2',
67 helperText: 'Protocol (https) and default port (8006) implied'),
68 controller: widget.originController,
69 validator: widget.originValidator,
74 mainAxisAlignment: MainAxisAlignment.center,
77 decoration: InputDecoration(
78 icon: Icon(Icons.vpn_lock),
81 controller: widget.originController,
85 decoration: InputDecoration(
86 icon: Icon(Icons.person),
87 labelText: 'Username',
89 controller: widget.usernameController,
92 return 'Please enter username';
97 DropdownButtonFormField(
98 decoration: InputDecoration(icon: Icon(Icons.domain)),
99 items: widget.accessDomains
100 .map((e) => DropdownMenuItem(
102 title: Text(e.realm),
103 subtitle: Text(e.comment ?? ''),
108 onChanged: widget.onDomainChanged,
109 selectedItemBuilder: (context) =>
110 widget.accessDomains.map((e) => Text(e.realm)).toList(),
111 value: widget.selectedDomain,
116 decoration: InputDecoration(
117 icon: Icon(Icons.lock),
118 labelText: 'Password',
120 controller: widget.passwordController,
121 obscureText: _obscure,
123 focusNode: passwordFocusNode,
126 return 'Please enter password';
132 alignment: Alignment.bottomRight,
134 constraints: BoxConstraints.tight(Size(58, 58)),
136 icon: Icon(_obscure ? Icons.visibility : Icons.visibility_off),
137 onPressed: () => setState(() {
138 _obscure = !_obscure;
150 passwordFocusNode?.dispose();
155 class ProxmoxLoginPage extends StatefulWidget {
156 final ProxmoxLoginModel userModel;
159 const ProxmoxLoginPage({
163 }) : super(key: key);
165 _ProxmoxLoginPageState createState() => _ProxmoxLoginPageState();
168 class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
169 final _originController = TextEditingController();
170 final _usernameController = TextEditingController();
171 final _passwordController = TextEditingController();
172 Future<List<PveAccessDomainModel>> _accessDomains;
173 PveAccessDomainModel _selectedDomain;
174 final _formKey = GlobalKey<FormState>();
175 ProxmoxProgressModel _progressModel;
176 bool _submittButtonEnabled = true;
180 final userModel = widget.userModel;
181 _progressModel = ProxmoxProgressModel();
182 if (!widget.isCreate && userModel != null) {
185 ..message = 'Connection test...';
186 _originController.text =
187 '${userModel.origin?.host}:${userModel.origin?.port}';
188 _accessDomains = _getAccessDomains();
189 _usernameController.text = userModel.username;
194 Widget build(BuildContext context) {
196 data: ThemeData.dark().copyWith(accentColor: Color(0xFFE47225)),
198 color: Theme.of(context).primaryColor,
201 SingleChildScrollView(
202 child: ConstrainedBox(
203 constraints: BoxConstraints.tightFor(
204 height: MediaQuery.of(context).size.height),
206 padding: const EdgeInsets.all(8.0),
207 child: FutureBuilder<List<PveAccessDomainModel>>(
208 future: _accessDomains,
209 builder: (context, snapshot) {
214 _submittButtonEnabled =
215 _formKey.currentState.validate();
219 mainAxisAlignment: MainAxisAlignment.center,
224 mainAxisAlignment: MainAxisAlignment.center,
229 fontFamily: 'Proxmox',
244 originController: _originController,
245 originValidator: (value) {
247 return 'Please enter origin';
249 if (value.startsWith('https://') ||
250 value.startsWith('http://')) {
251 return 'Do not prefix with scheme';
254 Uri.https(value, '');
256 } on FormatException catch (_) {
257 return 'Invalid URI';
260 usernameController: _usernameController,
261 passwordController: _passwordController,
262 accessDomains: snapshot.data,
263 selectedDomain: _selectedDomain,
264 onDomainChanged: (value) {
266 _selectedDomain = value;
270 if (snapshot.hasData)
273 alignment: Alignment.bottomCenter,
275 width: MediaQuery.of(context).size.width,
277 onPressed: _submittButtonEnabled
279 final isValid = _formKey
283 _submittButtonEnabled =
287 _onLoginButtonPressed();
291 color: Color(0xFFE47225),
292 disabledColor: Colors.grey,
293 child: Text('Continue'),
298 if (!snapshot.hasData)
301 alignment: Alignment.bottomCenter,
303 width: MediaQuery.of(context).size.width,
305 onPressed: _submittButtonEnabled
307 final isValid = _formKey
311 _submittButtonEnabled =
322 color: Color(0xFFE47225),
323 child: Text('Continue'),
324 disabledColor: Colors.grey,
336 if (_progressModel.inProgress)
337 ProxmoxProgressOverlay(message: _progressModel.message),
344 Future<void> _onLoginButtonPressed() async {
348 ..message = 'Authenticating...';
352 final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
354 //cleaned form fields
355 final origin = Uri.https(_originController.text.trim(), '');
356 final username = _usernameController.text.trim();
357 final password = _passwordController.text.trim();
358 final realm = _selectedDomain.realm;
360 var client = await proxclient.authenticate(
361 '$username@$realm', password, origin, settings.sslValidation);
363 if (client.credentials.tfa) {
364 client = await Navigator.of(context).push(MaterialPageRoute(
365 builder: (context) => ProxmoxTfaForm(
371 var loginStorage = await ProxmoxLoginStorage.fromLocalStorage();
373 if (widget.isCreate) {
374 final newLogin = ProxmoxLoginModel((b) => b
376 ..username = username
378 ..productType = ProxmoxProductType.pve
379 ..ticket = client.credentials.ticket);
381 loginStorage = loginStorage.rebuild((b) => b..logins.add(newLogin));
383 loginStorage = loginStorage.rebuild((b) => b
384 ..logins.remove(widget.userModel)
385 ..logins.add(widget.userModel
386 .rebuild((b) => b..ticket = client.credentials.ticket)));
388 await loginStorage.saveToDisk();
390 Navigator.of(context).pop(client);
391 } on proxclient.ProxmoxApiException catch (e) {
395 builder: (context) => ProxmoxApiErrorDialog(
402 if (e.runtimeType == HandshakeException) {
405 builder: (context) => ProxmoxCertificateErrorDialog(),
410 _progressModel.inProgress = false;
414 Future<List<PveAccessDomainModel>> _getAccessDomains() async {
418 ..message = 'Connection test...';
420 var apiBaseUrl = Uri.https(_originController.text.trim(), '');
422 if (!apiBaseUrl.hasPort) {
423 _originController.text += ':8006';
424 apiBaseUrl = apiBaseUrl.replace(port: 8006);
427 final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
428 List<PveAccessDomainModel> response;
431 await proxclient.accessDomains(apiBaseUrl, settings.sslValidation);
432 } on proxclient.ProxmoxApiException catch (e) {
436 builder: (context) => ProxmoxApiErrorDialog(
443 if (e.runtimeType == HandshakeException) {
446 builder: (context) => ProxmoxCertificateErrorDialog(),
451 builder: (context) => AlertDialog(
452 title: Text('Connection error'),
453 content: Text('Could not establish connection.'),
456 onPressed: () => Navigator.of(context).pop(),
457 child: Text('Close'),
465 response?.sort((a, b) => a.realm.compareTo(b.realm));
467 final selection = response?.singleWhere(
468 (e) => e.realm == widget.userModel?.realm,
469 orElse: () => response?.first,
473 _progressModel.inProgress = false;
474 _selectedDomain = selection;
482 _originController.dispose();
483 _usernameController.dispose();
484 _passwordController.dispose();
489 class ProxmoxProgressOverlay extends StatelessWidget {
490 const ProxmoxProgressOverlay({
492 @required this.message,
493 }) : super(key: key);
495 final String message;
498 Widget build(BuildContext context) {
500 decoration: new BoxDecoration(color: Colors.black.withOpacity(0.5)),
503 mainAxisAlignment: MainAxisAlignment.center,
508 color: Theme.of(context).accentColor,
513 padding: const EdgeInsets.only(top: 20.0),
514 child: CircularProgressIndicator(),
523 class ProxmoxApiErrorDialog extends StatelessWidget {
524 final proxclient.ProxmoxApiException exception;
526 const ProxmoxApiErrorDialog({
528 @required this.exception,
529 }) : super(key: key);
532 Widget build(BuildContext context) {
534 title: Text('API Error'),
535 content: SingleChildScrollView(
536 child: Text(exception.message),
540 onPressed: () => Navigator.of(context).pop(),
541 child: Text('Close'),
548 class ProxmoxCertificateErrorDialog extends StatelessWidget {
549 const ProxmoxCertificateErrorDialog({
551 }) : super(key: key);
554 Widget build(BuildContext context) {
556 title: Text('Certificate error'),
557 content: SingleChildScrollView(
559 crossAxisAlignment: CrossAxisAlignment.start,
561 Text('Your connection is not private.'),
563 'Note: Consider to disable SSL validation,'
564 ' if you use a self signed, not commonly trusted, certificate.',
565 style: Theme.of(context).textTheme.caption,
572 onPressed: () => Navigator.of(context).pop(),
573 child: Text('Close'),
576 onPressed: () => Navigator.of(context).pushReplacement(
578 builder: (context) => ProxmoxGeneralSettingsForm())),
579 child: Text('Settings'),