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