]>
Commit | Line | Data |
---|---|---|
0e468546 | 1 | import 'dart:io'; |
a9d1ee22 | 2 | import 'dart:async'; |
0e468546 TM |
3 | |
4 | import 'package:flutter/material.dart'; | |
f0291f5a | 5 | import 'package:collection/src/iterable_extensions.dart'; |
0e468546 TM |
6 | import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart' |
7 | as proxclient; | |
8 | import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart'; | |
9 | import 'package:proxmox_login_manager/proxmox_general_settings_form.dart'; | |
10 | import 'package:proxmox_login_manager/proxmox_general_settings_model.dart'; | |
11 | import 'package:proxmox_login_manager/proxmox_login_model.dart'; | |
12 | import 'package:proxmox_login_manager/proxmox_tfa_form.dart'; | |
1dfe0c76 | 13 | import 'package:proxmox_login_manager/extension.dart'; |
62145778 | 14 | import 'package:proxmox_login_manager/proxmox_password_store.dart'; |
0e468546 TM |
15 | |
16 | class ProxmoxProgressModel { | |
cff301db | 17 | int inProgress = 0; |
a9d1ee22 | 18 | String message = 'Loading...'; |
0e468546 | 19 | ProxmoxProgressModel({ |
cff301db | 20 | this.inProgress = 0, |
0e468546 TM |
21 | this.message = 'Loading...', |
22 | }); | |
23 | } | |
24 | ||
f0291f5a TL |
25 | // FIXME: copied from pve_flutter_frontend, re-use common set |
26 | class ProxmoxColors { | |
27 | static final Color orange = Color(0xFFE57000); | |
28 | static final Color supportGrey = Color(0xFFABBABA); | |
29 | static final Color supportBlue = Color(0xFF00617F); | |
30 | } | |
31 | ||
0e468546 TM |
32 | class ProxmoxLoginForm extends StatefulWidget { |
33 | final TextEditingController originController; | |
34 | final FormFieldValidator<String> originValidator; | |
35 | final TextEditingController usernameController; | |
36 | final TextEditingController passwordController; | |
a9d1ee22 TL |
37 | final List<PveAccessDomainModel?>? accessDomains; |
38 | final PveAccessDomainModel? selectedDomain; | |
39 | final ValueChanged<PveAccessDomainModel?> onDomainChanged; | |
40 | final Function? onPasswordSubmitted; | |
41 | final Function? onOriginSubmitted; | |
62145778 DC |
42 | final Function? onSavePasswordChanged; |
43 | final bool? canSavePassword; | |
44 | final bool? passwordSaved; | |
0e468546 TM |
45 | |
46 | const ProxmoxLoginForm({ | |
a9d1ee22 | 47 | Key? key, |
f0291f5a TL |
48 | required this.originController, |
49 | required this.usernameController, | |
50 | required this.passwordController, | |
51 | required this.accessDomains, | |
52 | required this.originValidator, | |
0e468546 | 53 | this.selectedDomain, |
f0291f5a | 54 | required this.onDomainChanged, |
c2ebed05 TM |
55 | this.onPasswordSubmitted, |
56 | this.onOriginSubmitted, | |
62145778 DC |
57 | this.onSavePasswordChanged, |
58 | this.canSavePassword, | |
59 | this.passwordSaved, | |
0e468546 TM |
60 | }) : super(key: key); |
61 | ||
62 | @override | |
63 | _ProxmoxLoginFormState createState() => _ProxmoxLoginFormState(); | |
64 | } | |
65 | ||
66 | class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> { | |
67 | bool _obscure = true; | |
62145778 | 68 | bool? _savePwCheckbox; |
a9d1ee22 | 69 | FocusNode? passwordFocusNode; |
0e468546 | 70 | |
0e468546 TM |
71 | @override |
72 | Widget build(BuildContext context) { | |
73 | if (widget.accessDomains == null) { | |
74 | return TextFormField( | |
75 | decoration: InputDecoration( | |
76 | icon: Icon(Icons.vpn_lock), | |
77 | labelText: 'Origin', | |
78 | hintText: 'e.g. 192.168.1.2', | |
f591bbbe DC |
79 | helperText: |
80 | 'Protocol (https) and default port (8006 or 443) implied'), | |
0e468546 TM |
81 | controller: widget.originController, |
82 | validator: widget.originValidator, | |
a9d1ee22 | 83 | onFieldSubmitted: (value) => widget.onOriginSubmitted!(), |
0e468546 TM |
84 | ); |
85 | } | |
86 | ||
eeba8b50 TM |
87 | return AutofillGroup( |
88 | child: Column( | |
89 | mainAxisAlignment: MainAxisAlignment.center, | |
90 | children: [ | |
91 | TextFormField( | |
92 | decoration: InputDecoration( | |
93 | icon: Icon(Icons.vpn_lock), | |
94 | labelText: 'Origin', | |
95 | ), | |
96 | controller: widget.originController, | |
97 | enabled: false, | |
0e468546 | 98 | ), |
eeba8b50 TM |
99 | TextFormField( |
100 | decoration: InputDecoration( | |
101 | icon: Icon(Icons.person), | |
102 | labelText: 'Username', | |
0e468546 | 103 | ), |
eeba8b50 TM |
104 | controller: widget.usernameController, |
105 | validator: (value) { | |
a9d1ee22 | 106 | if (value!.isEmpty) { |
eeba8b50 TM |
107 | return 'Please enter username'; |
108 | } | |
109 | return null; | |
110 | }, | |
111 | autofillHints: [AutofillHints.username], | |
112 | ), | |
113 | DropdownButtonFormField( | |
114 | decoration: InputDecoration(icon: Icon(Icons.domain)), | |
a9d1ee22 | 115 | items: widget.accessDomains! |
eeba8b50 TM |
116 | .map((e) => DropdownMenuItem( |
117 | child: ListTile( | |
f0291f5a | 118 | title: Text(e!.realm), |
eeba8b50 TM |
119 | subtitle: Text(e.comment ?? ''), |
120 | ), | |
121 | value: e, | |
122 | )) | |
123 | .toList(), | |
124 | onChanged: widget.onDomainChanged, | |
125 | selectedItemBuilder: (context) => | |
f0291f5a | 126 | widget.accessDomains!.map((e) => Text(e!.realm)).toList(), |
eeba8b50 TM |
127 | value: widget.selectedDomain, |
128 | ), | |
129 | Stack( | |
130 | children: [ | |
131 | TextFormField( | |
132 | decoration: InputDecoration( | |
133 | icon: Icon(Icons.lock), | |
134 | labelText: 'Password', | |
135 | ), | |
136 | controller: widget.passwordController, | |
137 | obscureText: _obscure, | |
138 | autocorrect: false, | |
139 | focusNode: passwordFocusNode, | |
140 | validator: (value) { | |
a9d1ee22 | 141 | if (value!.isEmpty) { |
eeba8b50 TM |
142 | return 'Please enter password'; |
143 | } | |
144 | return null; | |
145 | }, | |
a9d1ee22 | 146 | onFieldSubmitted: (value) => widget.onPasswordSubmitted!(), |
eeba8b50 | 147 | autofillHints: [AutofillHints.password], |
0e468546 | 148 | ), |
eeba8b50 TM |
149 | Align( |
150 | alignment: Alignment.bottomRight, | |
151 | child: IconButton( | |
152 | constraints: BoxConstraints.tight(Size(58, 58)), | |
153 | iconSize: 24, | |
154 | icon: | |
155 | Icon(_obscure ? Icons.visibility : Icons.visibility_off), | |
156 | onPressed: () => setState(() { | |
157 | _obscure = !_obscure; | |
158 | }), | |
159 | ), | |
160 | ) | |
161 | ], | |
162 | ), | |
62145778 DC |
163 | if (widget.canSavePassword ?? false) |
164 | CheckboxListTile( | |
165 | title: const Text('Save password'), | |
166 | value: _savePwCheckbox ?? widget.passwordSaved ?? false, | |
167 | onChanged: (value) { | |
168 | if (widget.onSavePasswordChanged != null) { | |
169 | widget.onSavePasswordChanged!(value!); | |
170 | } | |
171 | setState(() { | |
172 | _savePwCheckbox = value!; | |
173 | }); | |
174 | }, | |
175 | ) | |
eeba8b50 TM |
176 | ], |
177 | ), | |
0e468546 TM |
178 | ); |
179 | } | |
180 | ||
181 | @override | |
182 | void dispose() { | |
183 | passwordFocusNode?.dispose(); | |
184 | super.dispose(); | |
185 | } | |
186 | } | |
187 | ||
188 | class ProxmoxLoginPage extends StatefulWidget { | |
a9d1ee22 TL |
189 | final ProxmoxLoginModel? userModel; |
190 | final bool? isCreate; | |
191 | final String? ticket; | |
62145778 | 192 | final String? password; |
0e468546 TM |
193 | |
194 | const ProxmoxLoginPage({ | |
a9d1ee22 | 195 | Key? key, |
0e468546 TM |
196 | this.userModel, |
197 | this.isCreate, | |
1dfe0c76 | 198 | this.ticket = '', |
62145778 | 199 | this.password, |
0e468546 TM |
200 | }) : super(key: key); |
201 | @override | |
202 | _ProxmoxLoginPageState createState() => _ProxmoxLoginPageState(); | |
203 | } | |
204 | ||
205 | class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> { | |
206 | final _originController = TextEditingController(); | |
207 | final _usernameController = TextEditingController(); | |
208 | final _passwordController = TextEditingController(); | |
a9d1ee22 TL |
209 | Future<List<PveAccessDomainModel?>?>? _accessDomains; |
210 | PveAccessDomainModel? _selectedDomain; | |
0e468546 | 211 | final _formKey = GlobalKey<FormState>(); |
a9d1ee22 | 212 | ProxmoxProgressModel _progressModel = ProxmoxProgressModel(); |
0e468546 | 213 | bool _submittButtonEnabled = true; |
62145778 DC |
214 | bool _canSavePassword = false; |
215 | bool _savePasswordCB = false; | |
1dfe0c76 | 216 | |
0e468546 TM |
217 | @override |
218 | void initState() { | |
219 | super.initState(); | |
220 | final userModel = widget.userModel; | |
221 | _progressModel = ProxmoxProgressModel(); | |
a9d1ee22 | 222 | if (!widget.isCreate! && userModel != null) { |
e2515b17 | 223 | _originController.text = userModel.origin?.toString() ?? ''; |
f591bbbe DC |
224 | // Uri does not append 443 for https, so we do it manually |
225 | if (userModel.origin != null && | |
226 | userModel.origin!.scheme == "https" && | |
227 | userModel.origin!.port == 443) { | |
228 | _originController.text += ":443"; | |
229 | } | |
62145778 | 230 | _passwordController.text = widget.password ?? ''; |
0e468546 | 231 | _accessDomains = _getAccessDomains(); |
a9d1ee22 | 232 | _usernameController.text = userModel.username!; |
62145778 DC |
233 | _savePasswordCB = widget.password != null; |
234 | if ((widget.ticket!.isNotEmpty && userModel.activeSession) || | |
235 | widget.password != null) { | |
a9d1ee22 | 236 | _onLoginButtonPressed(ticket: widget.ticket!, mRealm: userModel.realm); |
1dfe0c76 | 237 | } |
0e468546 TM |
238 | } |
239 | } | |
240 | ||
241 | @override | |
242 | Widget build(BuildContext context) { | |
243 | return Theme( | |
f0291f5a TL |
244 | //data: ThemeData.dark().copyWith(accentColor: Color(0xFFE47225)), |
245 | data: ThemeData.dark().copyWith( | |
fcdfb148 DC |
246 | textButtonTheme: TextButtonThemeData( |
247 | style: TextButton.styleFrom( | |
248 | foregroundColor: Colors.white, | |
249 | backgroundColor: ProxmoxColors.orange, | |
250 | disabledBackgroundColor: Colors.grey, | |
251 | ), | |
252 | ), | |
62145778 | 253 | toggleableActiveColor: ProxmoxColors.orange, |
fcdfb148 | 254 | colorScheme: ColorScheme.dark().copyWith( |
2f3654f9 | 255 | primary: ProxmoxColors.orange, |
fcdfb148 | 256 | secondary: ProxmoxColors.orange, |
2f3654f9 | 257 | onSecondary: ProxmoxColors.supportGrey), |
fcdfb148 | 258 | ), |
5589fc9c | 259 | child: Scaffold( |
f0291f5a | 260 | backgroundColor: ProxmoxColors.supportBlue, |
ab9e420b TM |
261 | extendBodyBehindAppBar: true, |
262 | appBar: AppBar( | |
263 | elevation: 0.0, | |
264 | backgroundColor: Colors.transparent, | |
265 | leading: IconButton( | |
266 | icon: Icon(Icons.close), | |
267 | onPressed: () => Navigator.of(context).pop(), | |
268 | ), | |
269 | ), | |
5589fc9c | 270 | body: Stack( |
0e468546 TM |
271 | children: [ |
272 | SingleChildScrollView( | |
273 | child: ConstrainedBox( | |
274 | constraints: BoxConstraints.tightFor( | |
275 | height: MediaQuery.of(context).size.height), | |
959cc102 AL |
276 | child: SafeArea( |
277 | child: Padding( | |
278 | padding: const EdgeInsets.all(8.0), | |
279 | child: FutureBuilder<List<PveAccessDomainModel?>?>( | |
280 | future: _accessDomains, | |
281 | builder: (context, snapshot) { | |
282 | return Form( | |
283 | key: _formKey, | |
284 | onChanged: () { | |
285 | setState(() { | |
286 | _submittButtonEnabled = | |
287 | _formKey.currentState!.validate(); | |
288 | }); | |
289 | }, | |
290 | child: Column( | |
291 | mainAxisAlignment: MainAxisAlignment.center, | |
292 | children: [ | |
293 | Expanded( | |
294 | child: Container( | |
295 | child: Column( | |
296 | mainAxisAlignment: | |
297 | MainAxisAlignment.center, | |
298 | children: [ | |
299 | Image.asset( | |
300 | 'assets/images/proxmox_logo_symbol_wordmark.png', | |
301 | package: 'proxmox_login_manager', | |
302 | ), | |
303 | ], | |
304 | ), | |
0e468546 TM |
305 | ), |
306 | ), | |
959cc102 AL |
307 | ProxmoxLoginForm( |
308 | originController: _originController, | |
309 | originValidator: (value) { | |
310 | if (value == null || value.isEmpty) { | |
311 | return 'Please enter origin'; | |
312 | } | |
959cc102 | 313 | try { |
e2515b17 | 314 | normalizeUrl(value); |
959cc102 AL |
315 | return null; |
316 | } on FormatException catch (_) { | |
317 | return 'Invalid URI'; | |
e2515b17 DC |
318 | } on Exception catch (e) { |
319 | return 'Invalid URI: $e'; | |
959cc102 AL |
320 | } |
321 | }, | |
322 | usernameController: _usernameController, | |
323 | passwordController: _passwordController, | |
324 | accessDomains: snapshot.data, | |
325 | selectedDomain: _selectedDomain, | |
62145778 DC |
326 | onSavePasswordChanged: (value) { |
327 | _savePasswordCB = value; | |
328 | }, | |
329 | canSavePassword: _canSavePassword, | |
330 | passwordSaved: widget.password != null, | |
959cc102 AL |
331 | onDomainChanged: (value) { |
332 | setState(() { | |
333 | _selectedDomain = value; | |
334 | }); | |
335 | }, | |
336 | onOriginSubmitted: _submittButtonEnabled | |
337 | ? () { | |
338 | final isValid = | |
339 | _formKey.currentState!.validate(); | |
c2ebed05 | 340 | setState(() { |
959cc102 | 341 | _submittButtonEnabled = isValid; |
c2ebed05 | 342 | }); |
959cc102 AL |
343 | if (isValid) { |
344 | setState(() { | |
345 | _accessDomains = | |
346 | _getAccessDomains(); | |
347 | }); | |
348 | } | |
c2ebed05 | 349 | } |
959cc102 AL |
350 | : null, |
351 | onPasswordSubmitted: _submittButtonEnabled | |
352 | ? () { | |
353 | final isValid = | |
354 | _formKey.currentState!.validate(); | |
355 | setState(() { | |
356 | _submittButtonEnabled = isValid; | |
357 | }); | |
358 | if (isValid) { | |
359 | _onLoginButtonPressed(); | |
360 | } | |
c2ebed05 | 361 | } |
959cc102 AL |
362 | : null, |
363 | ), | |
a8758f87 DC |
364 | Expanded( |
365 | child: Align( | |
366 | alignment: Alignment.bottomCenter, | |
367 | child: Container( | |
368 | width: MediaQuery.of(context).size.width, | |
369 | child: TextButton( | |
370 | onPressed: _submittButtonEnabled | |
371 | ? () { | |
372 | final isValid = _formKey | |
373 | .currentState! | |
374 | .validate(); | |
375 | setState(() { | |
376 | _submittButtonEnabled = | |
377 | isValid; | |
378 | }); | |
379 | if (isValid) { | |
380 | if (snapshot.hasData) { | |
959cc102 | 381 | _onLoginButtonPressed(); |
a8758f87 | 382 | } else { |
959cc102 AL |
383 | setState(() { |
384 | _accessDomains = | |
385 | _getAccessDomains(); | |
386 | }); | |
387 | } | |
0e468546 | 388 | } |
a8758f87 DC |
389 | } |
390 | : null, | |
391 | child: Text('Continue'), | |
0e468546 TM |
392 | ), |
393 | ), | |
394 | ), | |
a8758f87 | 395 | ), |
959cc102 AL |
396 | ], |
397 | ), | |
398 | ); | |
399 | }), | |
400 | ), | |
0e468546 TM |
401 | ), |
402 | ), | |
403 | ), | |
cff301db | 404 | if (_progressModel.inProgress > 0) |
0e468546 TM |
405 | ProxmoxProgressOverlay(message: _progressModel.message), |
406 | ], | |
407 | ), | |
408 | ), | |
409 | ); | |
410 | } | |
411 | ||
1dfe0c76 | 412 | Future<void> _onLoginButtonPressed( |
a9d1ee22 | 413 | {String ticket = '', String? mRealm}) async { |
0e468546 TM |
414 | setState(() { |
415 | _progressModel | |
cff301db | 416 | ..inProgress += 1 |
0e468546 TM |
417 | ..message = 'Authenticating...'; |
418 | }); | |
419 | ||
420 | try { | |
421 | final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage(); | |
3752cb66 | 422 | //cleaned form fields |
e2515b17 | 423 | final origin = normalizeUrl(_originController.text.trim()); |
3752cb66 | 424 | final username = _usernameController.text.trim(); |
62145778 DC |
425 | final String enteredPassword = _passwordController.text.trim(); |
426 | final String? savedPassword = widget.password; | |
427 | ||
428 | final password = ticket.isNotEmpty ? ticket : enteredPassword; | |
1dfe0c76 | 429 | final realm = _selectedDomain?.realm ?? mRealm; |
3752cb66 | 430 | |
0e468546 | 431 | var client = await proxclient.authenticate( |
a9d1ee22 | 432 | '$username@$realm', password, origin, settings.sslValidation!); |
0e468546 | 433 | |
15d7e1f8 DC |
434 | if (client.credentials.tfa != null && |
435 | client.credentials.tfa!.kinds().length > 0) { | |
02c15546 DC |
436 | ProxmoxApiClient? tfaclient = |
437 | await Navigator.of(context).push(MaterialPageRoute( | |
0e468546 TM |
438 | builder: (context) => ProxmoxTfaForm( |
439 | apiClient: client, | |
440 | ), | |
df1d14b3 | 441 | )); |
02c15546 DC |
442 | |
443 | if (tfaclient != null) { | |
444 | client = tfaclient; | |
445 | } else { | |
446 | setState(() { | |
447 | _progressModel.inProgress -= 1; | |
448 | }); | |
449 | return; | |
450 | } | |
0e468546 TM |
451 | } |
452 | ||
1dfe0c76 TM |
453 | final status = await client.getClusterStatus(); |
454 | final hostname = | |
f0291f5a | 455 | status.singleWhereOrNull((element) => element.local ?? false)?.name; |
0e468546 TM |
456 | var loginStorage = await ProxmoxLoginStorage.fromLocalStorage(); |
457 | ||
62145778 DC |
458 | final savePW = enteredPassword != '' && |
459 | _savePasswordCB && | |
460 | enteredPassword != savedPassword; | |
461 | final deletePW = enteredPassword != '' && !savePW && !_savePasswordCB; | |
462 | String? id; | |
463 | ||
a9d1ee22 | 464 | if (widget.isCreate!) { |
0e468546 TM |
465 | final newLogin = ProxmoxLoginModel((b) => b |
466 | ..origin = origin | |
3752cb66 TM |
467 | ..username = username |
468 | ..realm = realm | |
0e468546 | 469 | ..productType = ProxmoxProductType.pve |
1dfe0c76 | 470 | ..ticket = client.credentials.ticket |
62145778 | 471 | ..passwordSaved = savePW |
1dfe0c76 | 472 | ..hostname = hostname); |
3752cb66 | 473 | |
a9d1ee22 | 474 | loginStorage = loginStorage!.rebuild((b) => b..logins.add(newLogin)); |
62145778 | 475 | id = newLogin.identifier; |
0e468546 | 476 | } else { |
a9d1ee22 | 477 | loginStorage = loginStorage!.rebuild((b) => b |
1dfe0c76 TM |
478 | ..logins.rebuildWhere( |
479 | (m) => m == widget.userModel, | |
480 | (b) => b | |
481 | ..ticket = client.credentials.ticket | |
62145778 DC |
482 | ..passwordSaved = |
483 | savePW || (deletePW ? false : b.passwordSaved ?? false) | |
1dfe0c76 | 484 | ..hostname = hostname)); |
62145778 DC |
485 | id = widget.userModel!.identifier; |
486 | } | |
487 | ||
488 | if (id != null) { | |
489 | if (savePW) { | |
490 | await savePassword(id, enteredPassword); | |
491 | } else if (deletePW) { | |
492 | await deletePassword(id); | |
493 | } | |
0e468546 TM |
494 | } |
495 | await loginStorage.saveToDisk(); | |
496 | ||
497 | Navigator.of(context).pop(client); | |
498 | } on proxclient.ProxmoxApiException catch (e) { | |
499 | print(e); | |
f6cf3349 TM |
500 | if (e.message.contains('No ticket')) { |
501 | showDialog( | |
502 | context: context, | |
503 | builder: (context) => AlertDialog( | |
504 | title: Text('Version Error'), | |
505 | content: Text( | |
506 | 'Proxmox VE version not supported, please update your instance to use this app.'), | |
507 | actions: [ | |
fcdfb148 | 508 | TextButton( |
f6cf3349 TM |
509 | onPressed: () => Navigator.of(context).pop(), |
510 | child: Text('Close'), | |
511 | ), | |
512 | ], | |
513 | ), | |
514 | ); | |
515 | } else { | |
516 | showDialog( | |
517 | context: context, | |
518 | builder: (context) => ProxmoxApiErrorDialog( | |
519 | exception: e, | |
520 | ), | |
521 | ); | |
522 | } | |
0e468546 TM |
523 | } catch (e, trace) { |
524 | print(e); | |
525 | print(trace); | |
526 | if (e.runtimeType == HandshakeException) { | |
527 | showDialog( | |
528 | context: context, | |
529 | builder: (context) => ProxmoxCertificateErrorDialog(), | |
530 | ); | |
f591bbbe DC |
531 | } else { |
532 | showDialog( | |
533 | context: context, | |
90a3b765 | 534 | builder: (context) => ConnectionErrorDialog(exception: e), |
f591bbbe | 535 | ); |
0e468546 TM |
536 | } |
537 | } | |
538 | setState(() { | |
cff301db | 539 | _progressModel.inProgress -= 1; |
0e468546 TM |
540 | }); |
541 | } | |
542 | ||
9e6eb549 | 543 | Future<List<PveAccessDomainModel?>?> _loadAccessDomains(Uri uri) async { |
f591bbbe DC |
544 | final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage(); |
545 | List<PveAccessDomainModel?>? response; | |
546 | try { | |
547 | response = await proxclient.accessDomains(uri, settings.sslValidation!); | |
548 | } on proxclient.ProxmoxApiException catch (e) { | |
549 | showDialog( | |
550 | context: context, | |
551 | builder: (context) => ProxmoxApiErrorDialog( | |
552 | exception: e, | |
553 | ), | |
554 | ); | |
9e6eb549 DC |
555 | } on HandshakeException { |
556 | showDialog( | |
557 | context: context, | |
558 | builder: (context) => ProxmoxCertificateErrorDialog(), | |
559 | ); | |
560 | } | |
561 | return response; | |
562 | } | |
563 | ||
564 | Future<List<PveAccessDomainModel?>?> _tryLoadAccessDomains(Uri uri) async { | |
565 | List<PveAccessDomainModel?>? response; | |
566 | try { | |
567 | response = await _loadAccessDomains(uri); | |
568 | } catch (e) { | |
569 | showDialog( | |
570 | context: context, | |
571 | builder: (context) => ConnectionErrorDialog( | |
572 | exception: e, | |
573 | ), | |
574 | ); | |
f591bbbe DC |
575 | } |
576 | return response; | |
577 | } | |
578 | ||
a9d1ee22 | 579 | Future<List<PveAccessDomainModel?>?> _getAccessDomains() async { |
0e468546 TM |
580 | setState(() { |
581 | _progressModel | |
cff301db | 582 | ..inProgress += 1 |
4f1019e4 | 583 | ..message = 'Connecting...'; |
0e468546 | 584 | }); |
62145778 | 585 | |
c9fc855f | 586 | final canSavePW = await canSavePassword(); |
62145778 | 587 | setState(() { |
c9fc855f | 588 | _canSavePassword = canSavePW; |
62145778 DC |
589 | }); |
590 | ||
59ab8956 | 591 | var host = _originController.text.trim(); |
e2515b17 | 592 | var apiBaseUrl = normalizeUrl(host); |
0e468546 | 593 | |
59ab8956 TL |
594 | RegExp portRE = new RegExp(r":\d{1,5}$"); |
595 | ||
a9d1ee22 | 596 | List<PveAccessDomainModel?>? response; |
f591bbbe | 597 | |
9e6eb549 DC |
598 | if (portRE.hasMatch(host)) { |
599 | response = await _tryLoadAccessDomains(apiBaseUrl); | |
600 | } else { | |
601 | // try to guess the port, 8006 first, and then 443 | |
602 | apiBaseUrl = apiBaseUrl.replace(port: 8006); | |
603 | try { | |
604 | response = await _loadAccessDomains(apiBaseUrl); | |
605 | if (response != null) { | |
606 | _originController.text = '$host:8006'; | |
607 | } | |
608 | } catch (e) { | |
f591bbbe DC |
609 | // we were no port given, and we couldn't reach on port 8006, retry with 443 |
610 | apiBaseUrl = apiBaseUrl.replace(port: 443); | |
9e6eb549 | 611 | response = await _tryLoadAccessDomains(apiBaseUrl); |
a9088853 AL |
612 | if (response != null) { |
613 | _originController.text = '$host:443'; | |
614 | } | |
0e468546 TM |
615 | } |
616 | } | |
3752cb66 | 617 | |
f0291f5a | 618 | response?.sort((a, b) => a!.realm.compareTo(b!.realm)); |
3752cb66 | 619 | |
0e468546 | 620 | final selection = response?.singleWhere( |
a9d1ee22 | 621 | (e) => e!.realm == widget.userModel?.realm, |
0e468546 TM |
622 | orElse: () => response?.first, |
623 | ); | |
3752cb66 | 624 | |
0e468546 | 625 | setState(() { |
cff301db | 626 | _progressModel.inProgress -= 1; |
0e468546 TM |
627 | _selectedDomain = selection; |
628 | }); | |
3752cb66 | 629 | |
0e468546 TM |
630 | return response; |
631 | } | |
632 | ||
633 | @override | |
634 | void dispose() { | |
635 | _originController.dispose(); | |
636 | _usernameController.dispose(); | |
637 | _passwordController.dispose(); | |
638 | super.dispose(); | |
639 | } | |
640 | } | |
641 | ||
642 | class ProxmoxProgressOverlay extends StatelessWidget { | |
643 | const ProxmoxProgressOverlay({ | |
a9d1ee22 TL |
644 | Key? key, |
645 | required this.message, | |
0e468546 TM |
646 | }) : super(key: key); |
647 | ||
648 | final String message; | |
649 | ||
650 | @override | |
651 | Widget build(BuildContext context) { | |
652 | return Container( | |
653 | decoration: new BoxDecoration(color: Colors.black.withOpacity(0.5)), | |
654 | child: Center( | |
655 | child: Column( | |
656 | mainAxisAlignment: MainAxisAlignment.center, | |
657 | children: [ | |
658 | Text( | |
659 | message, | |
660 | style: TextStyle( | |
0e468546 TM |
661 | fontSize: 20, |
662 | ), | |
663 | ), | |
664 | Padding( | |
665 | padding: const EdgeInsets.only(top: 20.0), | |
666 | child: CircularProgressIndicator(), | |
667 | ) | |
668 | ], | |
669 | ), | |
670 | ), | |
671 | ); | |
672 | } | |
673 | } | |
674 | ||
90a3b765 DC |
675 | class ConnectionErrorDialog extends StatelessWidget { |
676 | final exception; | |
677 | ||
678 | const ConnectionErrorDialog({ | |
679 | Key? key, | |
680 | required this.exception, | |
681 | }) : super(key: key); | |
682 | ||
683 | @override | |
684 | Widget build(BuildContext context) { | |
685 | return AlertDialog( | |
686 | title: Text('Connection error'), | |
687 | content: Text('Could not establish connection: ${this.exception}'), | |
688 | actions: [ | |
689 | TextButton( | |
690 | onPressed: () => Navigator.of(context).pop(), | |
691 | child: Text('Close'), | |
692 | ), | |
693 | ], | |
694 | ); | |
695 | } | |
696 | } | |
697 | ||
0e468546 TM |
698 | class ProxmoxApiErrorDialog extends StatelessWidget { |
699 | final proxclient.ProxmoxApiException exception; | |
700 | ||
701 | const ProxmoxApiErrorDialog({ | |
a9d1ee22 TL |
702 | Key? key, |
703 | required this.exception, | |
0e468546 TM |
704 | }) : super(key: key); |
705 | ||
706 | @override | |
707 | Widget build(BuildContext context) { | |
708 | return AlertDialog( | |
709 | title: Text('API Error'), | |
710 | content: SingleChildScrollView( | |
711 | child: Text(exception.message), | |
712 | ), | |
713 | actions: [ | |
fcdfb148 | 714 | TextButton( |
0e468546 TM |
715 | onPressed: () => Navigator.of(context).pop(), |
716 | child: Text('Close'), | |
717 | ), | |
718 | ], | |
719 | ); | |
720 | } | |
721 | } | |
722 | ||
723 | class ProxmoxCertificateErrorDialog extends StatelessWidget { | |
724 | const ProxmoxCertificateErrorDialog({ | |
a9d1ee22 | 725 | Key? key, |
0e468546 TM |
726 | }) : super(key: key); |
727 | ||
728 | @override | |
729 | Widget build(BuildContext context) { | |
730 | return AlertDialog( | |
731 | title: Text('Certificate error'), | |
732 | content: SingleChildScrollView( | |
733 | child: Column( | |
734 | crossAxisAlignment: CrossAxisAlignment.start, | |
735 | children: [ | |
736 | Text('Your connection is not private.'), | |
737 | Text( | |
738 | 'Note: Consider to disable SSL validation,' | |
739 | ' if you use a self signed, not commonly trusted, certificate.', | |
740 | style: Theme.of(context).textTheme.caption, | |
741 | ), | |
742 | ], | |
743 | ), | |
744 | ), | |
745 | actions: [ | |
fcdfb148 | 746 | TextButton( |
0e468546 TM |
747 | onPressed: () => Navigator.of(context).pop(), |
748 | child: Text('Close'), | |
749 | ), | |
fcdfb148 | 750 | TextButton( |
0e468546 TM |
751 | onPressed: () => Navigator.of(context).pushReplacement( |
752 | MaterialPageRoute( | |
753 | builder: (context) => ProxmoxGeneralSettingsForm())), | |
754 | child: Text('Settings'), | |
755 | ) | |
756 | ], | |
757 | ); | |
758 | } | |
759 | } | |
e2515b17 DC |
760 | |
761 | Uri normalizeUrl(String urlText) { | |
762 | if (urlText.startsWith('https://')) { | |
763 | urlText = urlText.substring('https://'.length); | |
764 | } | |
765 | if (urlText.startsWith('http://')) { | |
766 | throw new Exception("HTTP without TLS is not supported"); | |
767 | } | |
768 | ||
769 | return Uri.https(urlText, ''); | |
770 | } |