]>
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; | |
06ebbd20 | 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 | 55 | this.onPasswordSubmitted, |
06ebbd20 | 56 | required 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, | |
06ebbd20 | 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 | }, | |
06ebbd20 TL |
336 | onOriginSubmitted: () { |
337 | final isValid = | |
338 | _formKey.currentState!.validate(); | |
339 | setState(() { | |
340 | _submittButtonEnabled = isValid; | |
341 | }); | |
342 | if (isValid) { | |
343 | setState(() { | |
344 | _accessDomains = _getAccessDomains(); | |
345 | }); | |
346 | } | |
347 | }, | |
959cc102 AL |
348 | onPasswordSubmitted: _submittButtonEnabled |
349 | ? () { | |
350 | final isValid = | |
351 | _formKey.currentState!.validate(); | |
352 | setState(() { | |
353 | _submittButtonEnabled = isValid; | |
354 | }); | |
355 | if (isValid) { | |
356 | _onLoginButtonPressed(); | |
357 | } | |
c2ebed05 | 358 | } |
959cc102 AL |
359 | : null, |
360 | ), | |
a8758f87 DC |
361 | Expanded( |
362 | child: Align( | |
363 | alignment: Alignment.bottomCenter, | |
364 | child: Container( | |
365 | width: MediaQuery.of(context).size.width, | |
366 | child: TextButton( | |
367 | onPressed: _submittButtonEnabled | |
368 | ? () { | |
369 | final isValid = _formKey | |
370 | .currentState! | |
371 | .validate(); | |
372 | setState(() { | |
373 | _submittButtonEnabled = | |
374 | isValid; | |
375 | }); | |
376 | if (isValid) { | |
377 | if (snapshot.hasData) { | |
959cc102 | 378 | _onLoginButtonPressed(); |
a8758f87 | 379 | } else { |
959cc102 AL |
380 | setState(() { |
381 | _accessDomains = | |
382 | _getAccessDomains(); | |
383 | }); | |
384 | } | |
0e468546 | 385 | } |
a8758f87 DC |
386 | } |
387 | : null, | |
388 | child: Text('Continue'), | |
0e468546 TM |
389 | ), |
390 | ), | |
391 | ), | |
a8758f87 | 392 | ), |
959cc102 AL |
393 | ], |
394 | ), | |
395 | ); | |
396 | }), | |
397 | ), | |
0e468546 TM |
398 | ), |
399 | ), | |
400 | ), | |
cff301db | 401 | if (_progressModel.inProgress > 0) |
0e468546 TM |
402 | ProxmoxProgressOverlay(message: _progressModel.message), |
403 | ], | |
404 | ), | |
405 | ), | |
406 | ); | |
407 | } | |
408 | ||
1dfe0c76 | 409 | Future<void> _onLoginButtonPressed( |
a9d1ee22 | 410 | {String ticket = '', String? mRealm}) async { |
0e468546 TM |
411 | setState(() { |
412 | _progressModel | |
cff301db | 413 | ..inProgress += 1 |
0e468546 TM |
414 | ..message = 'Authenticating...'; |
415 | }); | |
416 | ||
417 | try { | |
418 | final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage(); | |
3752cb66 | 419 | //cleaned form fields |
e2515b17 | 420 | final origin = normalizeUrl(_originController.text.trim()); |
3752cb66 | 421 | final username = _usernameController.text.trim(); |
62145778 DC |
422 | final String enteredPassword = _passwordController.text.trim(); |
423 | final String? savedPassword = widget.password; | |
424 | ||
425 | final password = ticket.isNotEmpty ? ticket : enteredPassword; | |
1dfe0c76 | 426 | final realm = _selectedDomain?.realm ?? mRealm; |
3752cb66 | 427 | |
0e468546 | 428 | var client = await proxclient.authenticate( |
a9d1ee22 | 429 | '$username@$realm', password, origin, settings.sslValidation!); |
0e468546 | 430 | |
15d7e1f8 DC |
431 | if (client.credentials.tfa != null && |
432 | client.credentials.tfa!.kinds().length > 0) { | |
02c15546 DC |
433 | ProxmoxApiClient? tfaclient = |
434 | await Navigator.of(context).push(MaterialPageRoute( | |
0e468546 TM |
435 | builder: (context) => ProxmoxTfaForm( |
436 | apiClient: client, | |
437 | ), | |
df1d14b3 | 438 | )); |
02c15546 DC |
439 | |
440 | if (tfaclient != null) { | |
441 | client = tfaclient; | |
442 | } else { | |
443 | setState(() { | |
444 | _progressModel.inProgress -= 1; | |
445 | }); | |
446 | return; | |
447 | } | |
0e468546 TM |
448 | } |
449 | ||
1dfe0c76 TM |
450 | final status = await client.getClusterStatus(); |
451 | final hostname = | |
f0291f5a | 452 | status.singleWhereOrNull((element) => element.local ?? false)?.name; |
0e468546 TM |
453 | var loginStorage = await ProxmoxLoginStorage.fromLocalStorage(); |
454 | ||
62145778 DC |
455 | final savePW = enteredPassword != '' && |
456 | _savePasswordCB && | |
457 | enteredPassword != savedPassword; | |
458 | final deletePW = enteredPassword != '' && !savePW && !_savePasswordCB; | |
459 | String? id; | |
460 | ||
a9d1ee22 | 461 | if (widget.isCreate!) { |
0e468546 TM |
462 | final newLogin = ProxmoxLoginModel((b) => b |
463 | ..origin = origin | |
3752cb66 TM |
464 | ..username = username |
465 | ..realm = realm | |
0e468546 | 466 | ..productType = ProxmoxProductType.pve |
1dfe0c76 | 467 | ..ticket = client.credentials.ticket |
62145778 | 468 | ..passwordSaved = savePW |
1dfe0c76 | 469 | ..hostname = hostname); |
3752cb66 | 470 | |
a9d1ee22 | 471 | loginStorage = loginStorage!.rebuild((b) => b..logins.add(newLogin)); |
62145778 | 472 | id = newLogin.identifier; |
0e468546 | 473 | } else { |
a9d1ee22 | 474 | loginStorage = loginStorage!.rebuild((b) => b |
1dfe0c76 TM |
475 | ..logins.rebuildWhere( |
476 | (m) => m == widget.userModel, | |
477 | (b) => b | |
478 | ..ticket = client.credentials.ticket | |
62145778 DC |
479 | ..passwordSaved = |
480 | savePW || (deletePW ? false : b.passwordSaved ?? false) | |
1dfe0c76 | 481 | ..hostname = hostname)); |
62145778 DC |
482 | id = widget.userModel!.identifier; |
483 | } | |
484 | ||
485 | if (id != null) { | |
486 | if (savePW) { | |
487 | await savePassword(id, enteredPassword); | |
488 | } else if (deletePW) { | |
489 | await deletePassword(id); | |
490 | } | |
0e468546 TM |
491 | } |
492 | await loginStorage.saveToDisk(); | |
493 | ||
494 | Navigator.of(context).pop(client); | |
495 | } on proxclient.ProxmoxApiException catch (e) { | |
496 | print(e); | |
f6cf3349 TM |
497 | if (e.message.contains('No ticket')) { |
498 | showDialog( | |
499 | context: context, | |
500 | builder: (context) => AlertDialog( | |
501 | title: Text('Version Error'), | |
502 | content: Text( | |
503 | 'Proxmox VE version not supported, please update your instance to use this app.'), | |
504 | actions: [ | |
fcdfb148 | 505 | TextButton( |
f6cf3349 TM |
506 | onPressed: () => Navigator.of(context).pop(), |
507 | child: Text('Close'), | |
508 | ), | |
509 | ], | |
510 | ), | |
511 | ); | |
512 | } else { | |
513 | showDialog( | |
514 | context: context, | |
515 | builder: (context) => ProxmoxApiErrorDialog( | |
516 | exception: e, | |
517 | ), | |
518 | ); | |
519 | } | |
0e468546 TM |
520 | } catch (e, trace) { |
521 | print(e); | |
522 | print(trace); | |
523 | if (e.runtimeType == HandshakeException) { | |
524 | showDialog( | |
525 | context: context, | |
526 | builder: (context) => ProxmoxCertificateErrorDialog(), | |
527 | ); | |
f591bbbe DC |
528 | } else { |
529 | showDialog( | |
530 | context: context, | |
90a3b765 | 531 | builder: (context) => ConnectionErrorDialog(exception: e), |
f591bbbe | 532 | ); |
0e468546 TM |
533 | } |
534 | } | |
535 | setState(() { | |
cff301db | 536 | _progressModel.inProgress -= 1; |
0e468546 TM |
537 | }); |
538 | } | |
539 | ||
9e6eb549 | 540 | Future<List<PveAccessDomainModel?>?> _loadAccessDomains(Uri uri) async { |
f591bbbe DC |
541 | final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage(); |
542 | List<PveAccessDomainModel?>? response; | |
543 | try { | |
544 | response = await proxclient.accessDomains(uri, settings.sslValidation!); | |
545 | } on proxclient.ProxmoxApiException catch (e) { | |
546 | showDialog( | |
547 | context: context, | |
548 | builder: (context) => ProxmoxApiErrorDialog( | |
549 | exception: e, | |
550 | ), | |
551 | ); | |
9e6eb549 DC |
552 | } on HandshakeException { |
553 | showDialog( | |
554 | context: context, | |
555 | builder: (context) => ProxmoxCertificateErrorDialog(), | |
556 | ); | |
557 | } | |
558 | return response; | |
559 | } | |
560 | ||
561 | Future<List<PveAccessDomainModel?>?> _tryLoadAccessDomains(Uri uri) async { | |
562 | List<PveAccessDomainModel?>? response; | |
563 | try { | |
564 | response = await _loadAccessDomains(uri); | |
565 | } catch (e) { | |
566 | showDialog( | |
567 | context: context, | |
568 | builder: (context) => ConnectionErrorDialog( | |
569 | exception: e, | |
570 | ), | |
571 | ); | |
f591bbbe DC |
572 | } |
573 | return response; | |
574 | } | |
575 | ||
a9d1ee22 | 576 | Future<List<PveAccessDomainModel?>?> _getAccessDomains() async { |
0e468546 TM |
577 | setState(() { |
578 | _progressModel | |
cff301db | 579 | ..inProgress += 1 |
4f1019e4 | 580 | ..message = 'Connecting...'; |
0e468546 | 581 | }); |
62145778 | 582 | |
c9fc855f | 583 | final canSavePW = await canSavePassword(); |
62145778 | 584 | setState(() { |
c9fc855f | 585 | _canSavePassword = canSavePW; |
62145778 DC |
586 | }); |
587 | ||
59ab8956 | 588 | var host = _originController.text.trim(); |
e2515b17 | 589 | var apiBaseUrl = normalizeUrl(host); |
0e468546 | 590 | |
59ab8956 TL |
591 | RegExp portRE = new RegExp(r":\d{1,5}$"); |
592 | ||
a9d1ee22 | 593 | List<PveAccessDomainModel?>? response; |
f591bbbe | 594 | |
9e6eb549 DC |
595 | if (portRE.hasMatch(host)) { |
596 | response = await _tryLoadAccessDomains(apiBaseUrl); | |
597 | } else { | |
598 | // try to guess the port, 8006 first, and then 443 | |
599 | apiBaseUrl = apiBaseUrl.replace(port: 8006); | |
600 | try { | |
601 | response = await _loadAccessDomains(apiBaseUrl); | |
602 | if (response != null) { | |
603 | _originController.text = '$host:8006'; | |
604 | } | |
605 | } catch (e) { | |
f591bbbe DC |
606 | // we were no port given, and we couldn't reach on port 8006, retry with 443 |
607 | apiBaseUrl = apiBaseUrl.replace(port: 443); | |
9e6eb549 | 608 | response = await _tryLoadAccessDomains(apiBaseUrl); |
a9088853 AL |
609 | if (response != null) { |
610 | _originController.text = '$host:443'; | |
611 | } | |
0e468546 TM |
612 | } |
613 | } | |
3752cb66 | 614 | |
f0291f5a | 615 | response?.sort((a, b) => a!.realm.compareTo(b!.realm)); |
3752cb66 | 616 | |
0e468546 | 617 | final selection = response?.singleWhere( |
a9d1ee22 | 618 | (e) => e!.realm == widget.userModel?.realm, |
0e468546 TM |
619 | orElse: () => response?.first, |
620 | ); | |
3752cb66 | 621 | |
0e468546 | 622 | setState(() { |
cff301db | 623 | _progressModel.inProgress -= 1; |
0e468546 TM |
624 | _selectedDomain = selection; |
625 | }); | |
3752cb66 | 626 | |
0e468546 TM |
627 | return response; |
628 | } | |
629 | ||
630 | @override | |
631 | void dispose() { | |
632 | _originController.dispose(); | |
633 | _usernameController.dispose(); | |
634 | _passwordController.dispose(); | |
635 | super.dispose(); | |
636 | } | |
637 | } | |
638 | ||
639 | class ProxmoxProgressOverlay extends StatelessWidget { | |
640 | const ProxmoxProgressOverlay({ | |
a9d1ee22 TL |
641 | Key? key, |
642 | required this.message, | |
0e468546 TM |
643 | }) : super(key: key); |
644 | ||
645 | final String message; | |
646 | ||
647 | @override | |
648 | Widget build(BuildContext context) { | |
649 | return Container( | |
650 | decoration: new BoxDecoration(color: Colors.black.withOpacity(0.5)), | |
651 | child: Center( | |
652 | child: Column( | |
653 | mainAxisAlignment: MainAxisAlignment.center, | |
654 | children: [ | |
655 | Text( | |
656 | message, | |
657 | style: TextStyle( | |
0e468546 TM |
658 | fontSize: 20, |
659 | ), | |
660 | ), | |
661 | Padding( | |
662 | padding: const EdgeInsets.only(top: 20.0), | |
663 | child: CircularProgressIndicator(), | |
664 | ) | |
665 | ], | |
666 | ), | |
667 | ), | |
668 | ); | |
669 | } | |
670 | } | |
671 | ||
90a3b765 DC |
672 | class ConnectionErrorDialog extends StatelessWidget { |
673 | final exception; | |
674 | ||
675 | const ConnectionErrorDialog({ | |
676 | Key? key, | |
677 | required this.exception, | |
678 | }) : super(key: key); | |
679 | ||
680 | @override | |
681 | Widget build(BuildContext context) { | |
682 | return AlertDialog( | |
683 | title: Text('Connection error'), | |
684 | content: Text('Could not establish connection: ${this.exception}'), | |
685 | actions: [ | |
686 | TextButton( | |
687 | onPressed: () => Navigator.of(context).pop(), | |
688 | child: Text('Close'), | |
689 | ), | |
690 | ], | |
691 | ); | |
692 | } | |
693 | } | |
694 | ||
0e468546 TM |
695 | class ProxmoxApiErrorDialog extends StatelessWidget { |
696 | final proxclient.ProxmoxApiException exception; | |
697 | ||
698 | const ProxmoxApiErrorDialog({ | |
a9d1ee22 TL |
699 | Key? key, |
700 | required this.exception, | |
0e468546 TM |
701 | }) : super(key: key); |
702 | ||
703 | @override | |
704 | Widget build(BuildContext context) { | |
705 | return AlertDialog( | |
706 | title: Text('API Error'), | |
707 | content: SingleChildScrollView( | |
708 | child: Text(exception.message), | |
709 | ), | |
710 | actions: [ | |
fcdfb148 | 711 | TextButton( |
0e468546 TM |
712 | onPressed: () => Navigator.of(context).pop(), |
713 | child: Text('Close'), | |
714 | ), | |
715 | ], | |
716 | ); | |
717 | } | |
718 | } | |
719 | ||
720 | class ProxmoxCertificateErrorDialog extends StatelessWidget { | |
721 | const ProxmoxCertificateErrorDialog({ | |
a9d1ee22 | 722 | Key? key, |
0e468546 TM |
723 | }) : super(key: key); |
724 | ||
725 | @override | |
726 | Widget build(BuildContext context) { | |
727 | return AlertDialog( | |
728 | title: Text('Certificate error'), | |
729 | content: SingleChildScrollView( | |
730 | child: Column( | |
731 | crossAxisAlignment: CrossAxisAlignment.start, | |
732 | children: [ | |
733 | Text('Your connection is not private.'), | |
734 | Text( | |
735 | 'Note: Consider to disable SSL validation,' | |
736 | ' if you use a self signed, not commonly trusted, certificate.', | |
737 | style: Theme.of(context).textTheme.caption, | |
738 | ), | |
739 | ], | |
740 | ), | |
741 | ), | |
742 | actions: [ | |
fcdfb148 | 743 | TextButton( |
0e468546 TM |
744 | onPressed: () => Navigator.of(context).pop(), |
745 | child: Text('Close'), | |
746 | ), | |
fcdfb148 | 747 | TextButton( |
0e468546 TM |
748 | onPressed: () => Navigator.of(context).pushReplacement( |
749 | MaterialPageRoute( | |
750 | builder: (context) => ProxmoxGeneralSettingsForm())), | |
751 | child: Text('Settings'), | |
752 | ) | |
753 | ], | |
754 | ); | |
755 | } | |
756 | } | |
e2515b17 DC |
757 | |
758 | Uri normalizeUrl(String urlText) { | |
759 | if (urlText.startsWith('https://')) { | |
760 | urlText = urlText.substring('https://'.length); | |
761 | } | |
762 | if (urlText.startsWith('http://')) { | |
763 | throw new Exception("HTTP without TLS is not supported"); | |
764 | } | |
765 | ||
766 | return Uri.https(urlText, ''); | |
767 | } |