]>
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 { | |
7c043135 TL |
27 | static const Color orange = Color(0xFFE57000); |
28 | static const Color supportGrey = Color(0xFFABBABA); | |
29 | static const Color supportBlue = Color(0xFF00617F); | |
f0291f5a TL |
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 | |
f82ea945 | 63 | State<ProxmoxLoginForm> createState() => _ProxmoxLoginFormState(); |
0e468546 TM |
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( | |
7c043135 | 75 | decoration: const InputDecoration( |
0e468546 TM |
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'), | |
3e95b235 | 81 | textInputAction: TextInputAction.next, |
0e468546 TM |
82 | controller: widget.originController, |
83 | validator: widget.originValidator, | |
06ebbd20 | 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( | |
7c043135 | 93 | decoration: const InputDecoration( |
eeba8b50 TM |
94 | icon: Icon(Icons.vpn_lock), |
95 | labelText: 'Origin', | |
96 | ), | |
97 | controller: widget.originController, | |
98 | enabled: false, | |
0e468546 | 99 | ), |
eeba8b50 | 100 | TextFormField( |
7c043135 | 101 | decoration: const InputDecoration( |
eeba8b50 TM |
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 | }, | |
7824e4fc | 112 | autofillHints: const [AutofillHints.username], |
eeba8b50 TM |
113 | ), |
114 | DropdownButtonFormField( | |
7c043135 | 115 | decoration: const InputDecoration(icon: Icon(Icons.domain)), |
a9d1ee22 | 116 | items: widget.accessDomains! |
eeba8b50 | 117 | .map((e) => DropdownMenuItem( |
7824e4fc | 118 | value: e, |
eeba8b50 | 119 | child: ListTile( |
f0291f5a | 120 | title: Text(e!.realm), |
eeba8b50 TM |
121 | subtitle: Text(e.comment ?? ''), |
122 | ), | |
eeba8b50 TM |
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( | |
7c043135 | 133 | decoration: const InputDecoration( |
eeba8b50 TM |
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!(), |
7824e4fc | 148 | autofillHints: const [AutofillHints.password], |
0e468546 | 149 | ), |
eeba8b50 TM |
150 | Align( |
151 | alignment: Alignment.bottomRight, | |
152 | child: IconButton( | |
7c043135 | 153 | constraints: BoxConstraints.tight(const Size(58, 58)), |
eeba8b50 TM |
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 | |
f82ea945 | 203 | State<ProxmoxLoginPage> createState() => _ProxmoxLoginPageState(); |
0e468546 TM |
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 | ), | |
7c043135 | 254 | colorScheme: const ColorScheme.dark().copyWith( |
2f3654f9 | 255 | primary: ProxmoxColors.orange, |
fcdfb148 | 256 | secondary: ProxmoxColors.orange, |
2f3654f9 | 257 | onSecondary: ProxmoxColors.supportGrey), |
f37413f7 TL |
258 | checkboxTheme: CheckboxThemeData( |
259 | fillColor: MaterialStateProperty.resolveWith<Color?>( | |
260 | (Set<MaterialState> states) { | |
261 | if (states.contains(MaterialState.disabled)) { | |
262 | return null; | |
263 | } | |
264 | if (states.contains(MaterialState.selected)) { | |
265 | return ProxmoxColors.orange; | |
266 | } | |
267 | return null; | |
268 | }), | |
269 | ), | |
270 | radioTheme: RadioThemeData( | |
271 | fillColor: MaterialStateProperty.resolveWith<Color?>( | |
272 | (Set<MaterialState> states) { | |
273 | if (states.contains(MaterialState.disabled)) { | |
274 | return null; | |
275 | } | |
276 | if (states.contains(MaterialState.selected)) { | |
277 | return ProxmoxColors.orange; | |
278 | } | |
279 | return null; | |
280 | }), | |
281 | ), | |
282 | switchTheme: SwitchThemeData( | |
283 | thumbColor: MaterialStateProperty.resolveWith<Color?>( | |
284 | (Set<MaterialState> states) { | |
285 | if (states.contains(MaterialState.disabled)) { | |
286 | return null; | |
287 | } | |
288 | if (states.contains(MaterialState.selected)) { | |
289 | return ProxmoxColors.orange; | |
290 | } | |
291 | return null; | |
292 | }), | |
293 | trackColor: MaterialStateProperty.resolveWith<Color?>( | |
294 | (Set<MaterialState> states) { | |
295 | if (states.contains(MaterialState.disabled)) { | |
296 | return null; | |
297 | } | |
298 | if (states.contains(MaterialState.selected)) { | |
299 | return ProxmoxColors.orange; | |
300 | } | |
301 | return null; | |
302 | }), | |
303 | ), | |
fcdfb148 | 304 | ), |
5589fc9c | 305 | child: Scaffold( |
f0291f5a | 306 | backgroundColor: ProxmoxColors.supportBlue, |
ab9e420b TM |
307 | extendBodyBehindAppBar: true, |
308 | appBar: AppBar( | |
309 | elevation: 0.0, | |
310 | backgroundColor: Colors.transparent, | |
311 | leading: IconButton( | |
7c043135 | 312 | icon: const Icon(Icons.close), |
ab9e420b TM |
313 | onPressed: () => Navigator.of(context).pop(), |
314 | ), | |
315 | ), | |
5589fc9c | 316 | body: Stack( |
0e468546 TM |
317 | children: [ |
318 | SingleChildScrollView( | |
319 | child: ConstrainedBox( | |
320 | constraints: BoxConstraints.tightFor( | |
321 | height: MediaQuery.of(context).size.height), | |
959cc102 AL |
322 | child: SafeArea( |
323 | child: Padding( | |
324 | padding: const EdgeInsets.all(8.0), | |
325 | child: FutureBuilder<List<PveAccessDomainModel?>?>( | |
326 | future: _accessDomains, | |
327 | builder: (context, snapshot) { | |
328 | return Form( | |
329 | key: _formKey, | |
330 | onChanged: () { | |
331 | setState(() { | |
332 | _submittButtonEnabled = | |
333 | _formKey.currentState!.validate(); | |
334 | }); | |
335 | }, | |
336 | child: Column( | |
337 | mainAxisAlignment: MainAxisAlignment.center, | |
338 | children: [ | |
339 | Expanded( | |
340 | child: Container( | |
341 | child: Column( | |
342 | mainAxisAlignment: | |
343 | MainAxisAlignment.center, | |
344 | children: [ | |
345 | Image.asset( | |
346 | 'assets/images/proxmox_logo_symbol_wordmark.png', | |
347 | package: 'proxmox_login_manager', | |
348 | ), | |
349 | ], | |
350 | ), | |
0e468546 TM |
351 | ), |
352 | ), | |
959cc102 AL |
353 | ProxmoxLoginForm( |
354 | originController: _originController, | |
355 | originValidator: (value) { | |
356 | if (value == null || value.isEmpty) { | |
357 | return 'Please enter origin'; | |
358 | } | |
959cc102 | 359 | try { |
e2515b17 | 360 | normalizeUrl(value); |
959cc102 AL |
361 | return null; |
362 | } on FormatException catch (_) { | |
363 | return 'Invalid URI'; | |
e2515b17 DC |
364 | } on Exception catch (e) { |
365 | return 'Invalid URI: $e'; | |
959cc102 AL |
366 | } |
367 | }, | |
368 | usernameController: _usernameController, | |
369 | passwordController: _passwordController, | |
370 | accessDomains: snapshot.data, | |
371 | selectedDomain: _selectedDomain, | |
62145778 DC |
372 | onSavePasswordChanged: (value) { |
373 | _savePasswordCB = value; | |
374 | }, | |
375 | canSavePassword: _canSavePassword, | |
376 | passwordSaved: widget.password != null, | |
959cc102 AL |
377 | onDomainChanged: (value) { |
378 | setState(() { | |
379 | _selectedDomain = value; | |
380 | }); | |
381 | }, | |
06ebbd20 TL |
382 | onOriginSubmitted: () { |
383 | final isValid = | |
384 | _formKey.currentState!.validate(); | |
385 | setState(() { | |
386 | _submittButtonEnabled = isValid; | |
387 | }); | |
388 | if (isValid) { | |
389 | setState(() { | |
390 | _accessDomains = _getAccessDomains(); | |
391 | }); | |
392 | } | |
393 | }, | |
959cc102 AL |
394 | onPasswordSubmitted: _submittButtonEnabled |
395 | ? () { | |
396 | final isValid = | |
397 | _formKey.currentState!.validate(); | |
398 | setState(() { | |
399 | _submittButtonEnabled = isValid; | |
400 | }); | |
401 | if (isValid) { | |
402 | _onLoginButtonPressed(); | |
403 | } | |
c2ebed05 | 404 | } |
959cc102 AL |
405 | : null, |
406 | ), | |
a8758f87 DC |
407 | Expanded( |
408 | child: Align( | |
409 | alignment: Alignment.bottomCenter, | |
98bcbcae | 410 | child: SizedBox( |
a8758f87 DC |
411 | width: MediaQuery.of(context).size.width, |
412 | child: TextButton( | |
413 | onPressed: _submittButtonEnabled | |
414 | ? () { | |
415 | final isValid = _formKey | |
416 | .currentState! | |
417 | .validate(); | |
418 | setState(() { | |
419 | _submittButtonEnabled = | |
420 | isValid; | |
421 | }); | |
422 | if (isValid) { | |
423 | if (snapshot.hasData) { | |
959cc102 | 424 | _onLoginButtonPressed(); |
a8758f87 | 425 | } else { |
959cc102 AL |
426 | setState(() { |
427 | _accessDomains = | |
428 | _getAccessDomains(); | |
429 | }); | |
430 | } | |
0e468546 | 431 | } |
a8758f87 DC |
432 | } |
433 | : null, | |
7c043135 | 434 | child: const Text('Continue'), |
0e468546 TM |
435 | ), |
436 | ), | |
437 | ), | |
a8758f87 | 438 | ), |
959cc102 AL |
439 | ], |
440 | ), | |
441 | ); | |
442 | }), | |
443 | ), | |
0e468546 TM |
444 | ), |
445 | ), | |
446 | ), | |
cff301db | 447 | if (_progressModel.inProgress > 0) |
0e468546 TM |
448 | ProxmoxProgressOverlay(message: _progressModel.message), |
449 | ], | |
450 | ), | |
451 | ), | |
452 | ); | |
453 | } | |
454 | ||
1dfe0c76 | 455 | Future<void> _onLoginButtonPressed( |
a9d1ee22 | 456 | {String ticket = '', String? mRealm}) async { |
0e468546 TM |
457 | setState(() { |
458 | _progressModel | |
cff301db | 459 | ..inProgress += 1 |
0e468546 TM |
460 | ..message = 'Authenticating...'; |
461 | }); | |
462 | ||
463 | try { | |
464 | final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage(); | |
3752cb66 | 465 | //cleaned form fields |
e2515b17 | 466 | final origin = normalizeUrl(_originController.text.trim()); |
3752cb66 | 467 | final username = _usernameController.text.trim(); |
62145778 DC |
468 | final String enteredPassword = _passwordController.text.trim(); |
469 | final String? savedPassword = widget.password; | |
470 | ||
471 | final password = ticket.isNotEmpty ? ticket : enteredPassword; | |
1dfe0c76 | 472 | final realm = _selectedDomain?.realm ?? mRealm; |
3752cb66 | 473 | |
0e468546 | 474 | var client = await proxclient.authenticate( |
a9d1ee22 | 475 | '$username@$realm', password, origin, settings.sslValidation!); |
0e468546 | 476 | |
15d7e1f8 | 477 | if (client.credentials.tfa != null && |
7824e4fc | 478 | client.credentials.tfa!.kinds().isNotEmpty) { |
10b47afc | 479 | if (!mounted) return; |
02c15546 DC |
480 | ProxmoxApiClient? tfaclient = |
481 | await Navigator.of(context).push(MaterialPageRoute( | |
0e468546 TM |
482 | builder: (context) => ProxmoxTfaForm( |
483 | apiClient: client, | |
484 | ), | |
df1d14b3 | 485 | )); |
02c15546 DC |
486 | |
487 | if (tfaclient != null) { | |
488 | client = tfaclient; | |
489 | } else { | |
490 | setState(() { | |
491 | _progressModel.inProgress -= 1; | |
492 | }); | |
493 | return; | |
494 | } | |
0e468546 TM |
495 | } |
496 | ||
1dfe0c76 TM |
497 | final status = await client.getClusterStatus(); |
498 | final hostname = | |
f0291f5a | 499 | status.singleWhereOrNull((element) => element.local ?? false)?.name; |
0e468546 TM |
500 | var loginStorage = await ProxmoxLoginStorage.fromLocalStorage(); |
501 | ||
62145778 DC |
502 | final savePW = enteredPassword != '' && |
503 | _savePasswordCB && | |
504 | enteredPassword != savedPassword; | |
505 | final deletePW = enteredPassword != '' && !savePW && !_savePasswordCB; | |
506 | String? id; | |
507 | ||
a9d1ee22 | 508 | if (widget.isCreate!) { |
0e468546 TM |
509 | final newLogin = ProxmoxLoginModel((b) => b |
510 | ..origin = origin | |
3752cb66 TM |
511 | ..username = username |
512 | ..realm = realm | |
0e468546 | 513 | ..productType = ProxmoxProductType.pve |
1dfe0c76 | 514 | ..ticket = client.credentials.ticket |
62145778 | 515 | ..passwordSaved = savePW |
1dfe0c76 | 516 | ..hostname = hostname); |
3752cb66 | 517 | |
a9d1ee22 | 518 | loginStorage = loginStorage!.rebuild((b) => b..logins.add(newLogin)); |
62145778 | 519 | id = newLogin.identifier; |
0e468546 | 520 | } else { |
a9d1ee22 | 521 | loginStorage = loginStorage!.rebuild((b) => b |
1dfe0c76 TM |
522 | ..logins.rebuildWhere( |
523 | (m) => m == widget.userModel, | |
524 | (b) => b | |
525 | ..ticket = client.credentials.ticket | |
62145778 DC |
526 | ..passwordSaved = |
527 | savePW || (deletePW ? false : b.passwordSaved ?? false) | |
1dfe0c76 | 528 | ..hostname = hostname)); |
62145778 DC |
529 | id = widget.userModel!.identifier; |
530 | } | |
531 | ||
532 | if (id != null) { | |
533 | if (savePW) { | |
534 | await savePassword(id, enteredPassword); | |
535 | } else if (deletePW) { | |
536 | await deletePassword(id); | |
537 | } | |
0e468546 TM |
538 | } |
539 | await loginStorage.saveToDisk(); | |
540 | ||
10b47afc TL |
541 | if (mounted) { |
542 | Navigator.of(context).pop(client); | |
543 | } | |
0e468546 TM |
544 | } on proxclient.ProxmoxApiException catch (e) { |
545 | print(e); | |
10b47afc | 546 | if (!mounted) return; |
f6cf3349 TM |
547 | if (e.message.contains('No ticket')) { |
548 | showDialog( | |
549 | context: context, | |
550 | builder: (context) => AlertDialog( | |
7c043135 TL |
551 | title: const Text('Version Error'), |
552 | content: const Text( | |
f6cf3349 TM |
553 | 'Proxmox VE version not supported, please update your instance to use this app.'), |
554 | actions: [ | |
fcdfb148 | 555 | TextButton( |
f6cf3349 | 556 | onPressed: () => Navigator.of(context).pop(), |
7c043135 | 557 | child: const Text('Close'), |
f6cf3349 TM |
558 | ), |
559 | ], | |
560 | ), | |
561 | ); | |
562 | } else { | |
563 | showDialog( | |
564 | context: context, | |
565 | builder: (context) => ProxmoxApiErrorDialog( | |
566 | exception: e, | |
567 | ), | |
568 | ); | |
569 | } | |
0e468546 TM |
570 | } catch (e, trace) { |
571 | print(e); | |
572 | print(trace); | |
10b47afc TL |
573 | if (mounted) { |
574 | if (e.runtimeType == HandshakeException) { | |
575 | showDialog( | |
576 | context: context, | |
577 | builder: (context) => const ProxmoxCertificateErrorDialog(), | |
578 | ); | |
579 | } else { | |
580 | showDialog( | |
581 | context: context, | |
582 | builder: (context) => ConnectionErrorDialog(exception: e), | |
583 | ); | |
584 | } | |
0e468546 TM |
585 | } |
586 | } | |
587 | setState(() { | |
cff301db | 588 | _progressModel.inProgress -= 1; |
0e468546 TM |
589 | }); |
590 | } | |
591 | ||
9e6eb549 | 592 | Future<List<PveAccessDomainModel?>?> _loadAccessDomains(Uri uri) async { |
f591bbbe DC |
593 | final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage(); |
594 | List<PveAccessDomainModel?>? response; | |
595 | try { | |
596 | response = await proxclient.accessDomains(uri, settings.sslValidation!); | |
597 | } on proxclient.ProxmoxApiException catch (e) { | |
10b47afc TL |
598 | if (mounted) { |
599 | showDialog( | |
600 | context: context, | |
601 | builder: (context) => ProxmoxApiErrorDialog( | |
602 | exception: e, | |
603 | ), | |
604 | ); | |
605 | } | |
9e6eb549 | 606 | } on HandshakeException { |
10b47afc TL |
607 | if (mounted) { |
608 | showDialog( | |
609 | context: context, | |
610 | builder: (context) => const ProxmoxCertificateErrorDialog(), | |
611 | ); | |
612 | } | |
9e6eb549 DC |
613 | } |
614 | return response; | |
615 | } | |
616 | ||
617 | Future<List<PveAccessDomainModel?>?> _tryLoadAccessDomains(Uri uri) async { | |
618 | List<PveAccessDomainModel?>? response; | |
619 | try { | |
620 | response = await _loadAccessDomains(uri); | |
621 | } catch (e) { | |
10b47afc TL |
622 | if (mounted) { |
623 | showDialog( | |
624 | context: context, | |
625 | builder: (context) => ConnectionErrorDialog( | |
626 | exception: e, | |
627 | ), | |
628 | ); | |
629 | } | |
f591bbbe DC |
630 | } |
631 | return response; | |
632 | } | |
633 | ||
a9d1ee22 | 634 | Future<List<PveAccessDomainModel?>?> _getAccessDomains() async { |
0e468546 TM |
635 | setState(() { |
636 | _progressModel | |
cff301db | 637 | ..inProgress += 1 |
4f1019e4 | 638 | ..message = 'Connecting...'; |
0e468546 | 639 | }); |
62145778 | 640 | |
c9fc855f | 641 | final canSavePW = await canSavePassword(); |
62145778 | 642 | setState(() { |
c9fc855f | 643 | _canSavePassword = canSavePW; |
62145778 DC |
644 | }); |
645 | ||
59ab8956 | 646 | var host = _originController.text.trim(); |
e2515b17 | 647 | var apiBaseUrl = normalizeUrl(host); |
0e468546 | 648 | |
31126444 | 649 | RegExp portRE = RegExp(r":\d{1,5}$"); |
59ab8956 | 650 | |
a9d1ee22 | 651 | List<PveAccessDomainModel?>? response; |
f591bbbe | 652 | |
9e6eb549 DC |
653 | if (portRE.hasMatch(host)) { |
654 | response = await _tryLoadAccessDomains(apiBaseUrl); | |
655 | } else { | |
656 | // try to guess the port, 8006 first, and then 443 | |
657 | apiBaseUrl = apiBaseUrl.replace(port: 8006); | |
658 | try { | |
659 | response = await _loadAccessDomains(apiBaseUrl); | |
660 | if (response != null) { | |
661 | _originController.text = '$host:8006'; | |
662 | } | |
663 | } catch (e) { | |
f591bbbe DC |
664 | // we were no port given, and we couldn't reach on port 8006, retry with 443 |
665 | apiBaseUrl = apiBaseUrl.replace(port: 443); | |
9e6eb549 | 666 | response = await _tryLoadAccessDomains(apiBaseUrl); |
a9088853 AL |
667 | if (response != null) { |
668 | _originController.text = '$host:443'; | |
669 | } | |
0e468546 TM |
670 | } |
671 | } | |
3752cb66 | 672 | |
f0291f5a | 673 | response?.sort((a, b) => a!.realm.compareTo(b!.realm)); |
3752cb66 | 674 | |
0e468546 | 675 | final selection = response?.singleWhere( |
a9d1ee22 | 676 | (e) => e!.realm == widget.userModel?.realm, |
0e468546 TM |
677 | orElse: () => response?.first, |
678 | ); | |
3752cb66 | 679 | |
0e468546 | 680 | setState(() { |
cff301db | 681 | _progressModel.inProgress -= 1; |
0e468546 TM |
682 | _selectedDomain = selection; |
683 | }); | |
3752cb66 | 684 | |
0e468546 TM |
685 | return response; |
686 | } | |
687 | ||
688 | @override | |
689 | void dispose() { | |
690 | _originController.dispose(); | |
691 | _usernameController.dispose(); | |
692 | _passwordController.dispose(); | |
693 | super.dispose(); | |
694 | } | |
695 | } | |
696 | ||
697 | class ProxmoxProgressOverlay extends StatelessWidget { | |
698 | const ProxmoxProgressOverlay({ | |
a9d1ee22 TL |
699 | Key? key, |
700 | required this.message, | |
0e468546 TM |
701 | }) : super(key: key); |
702 | ||
703 | final String message; | |
704 | ||
705 | @override | |
706 | Widget build(BuildContext context) { | |
707 | return Container( | |
31126444 | 708 | decoration: BoxDecoration(color: Colors.black.withOpacity(0.5)), |
0e468546 TM |
709 | child: Center( |
710 | child: Column( | |
711 | mainAxisAlignment: MainAxisAlignment.center, | |
712 | children: [ | |
713 | Text( | |
714 | message, | |
7c043135 | 715 | style: const TextStyle( |
0e468546 TM |
716 | fontSize: 20, |
717 | ), | |
718 | ), | |
7c043135 TL |
719 | const Padding( |
720 | padding: EdgeInsets.only(top: 20.0), | |
0e468546 TM |
721 | child: CircularProgressIndicator(), |
722 | ) | |
723 | ], | |
724 | ), | |
725 | ), | |
726 | ); | |
727 | } | |
728 | } | |
729 | ||
90a3b765 | 730 | class ConnectionErrorDialog extends StatelessWidget { |
f82ea945 | 731 | final Object exception; |
90a3b765 DC |
732 | |
733 | const ConnectionErrorDialog({ | |
734 | Key? key, | |
735 | required this.exception, | |
736 | }) : super(key: key); | |
737 | ||
738 | @override | |
739 | Widget build(BuildContext context) { | |
740 | return AlertDialog( | |
7c043135 | 741 | title: const Text('Connection error'), |
217eb475 | 742 | content: Text('Could not establish connection: $exception'), |
90a3b765 DC |
743 | actions: [ |
744 | TextButton( | |
745 | onPressed: () => Navigator.of(context).pop(), | |
7c043135 | 746 | child: const Text('Close'), |
90a3b765 DC |
747 | ), |
748 | ], | |
749 | ); | |
750 | } | |
751 | } | |
752 | ||
0e468546 TM |
753 | class ProxmoxApiErrorDialog extends StatelessWidget { |
754 | final proxclient.ProxmoxApiException exception; | |
755 | ||
756 | const ProxmoxApiErrorDialog({ | |
a9d1ee22 TL |
757 | Key? key, |
758 | required this.exception, | |
0e468546 TM |
759 | }) : super(key: key); |
760 | ||
761 | @override | |
762 | Widget build(BuildContext context) { | |
763 | return AlertDialog( | |
7c043135 | 764 | title: const Text('API Error'), |
0e468546 TM |
765 | content: SingleChildScrollView( |
766 | child: Text(exception.message), | |
767 | ), | |
768 | actions: [ | |
fcdfb148 | 769 | TextButton( |
0e468546 | 770 | onPressed: () => Navigator.of(context).pop(), |
7c043135 | 771 | child: const Text('Close'), |
0e468546 TM |
772 | ), |
773 | ], | |
774 | ); | |
775 | } | |
776 | } | |
777 | ||
778 | class ProxmoxCertificateErrorDialog extends StatelessWidget { | |
779 | const ProxmoxCertificateErrorDialog({ | |
a9d1ee22 | 780 | Key? key, |
0e468546 TM |
781 | }) : super(key: key); |
782 | ||
783 | @override | |
784 | Widget build(BuildContext context) { | |
785 | return AlertDialog( | |
7c043135 | 786 | title: const Text('Certificate error'), |
0e468546 TM |
787 | content: SingleChildScrollView( |
788 | child: Column( | |
789 | crossAxisAlignment: CrossAxisAlignment.start, | |
790 | children: [ | |
7c043135 | 791 | const Text('Your connection is not private.'), |
0e468546 TM |
792 | Text( |
793 | 'Note: Consider to disable SSL validation,' | |
794 | ' if you use a self signed, not commonly trusted, certificate.', | |
f37413f7 | 795 | style: Theme.of(context).textTheme.bodySmall, |
0e468546 TM |
796 | ), |
797 | ], | |
798 | ), | |
799 | ), | |
800 | actions: [ | |
fcdfb148 | 801 | TextButton( |
0e468546 | 802 | onPressed: () => Navigator.of(context).pop(), |
7c043135 | 803 | child: const Text('Close'), |
0e468546 | 804 | ), |
fcdfb148 | 805 | TextButton( |
0e468546 TM |
806 | onPressed: () => Navigator.of(context).pushReplacement( |
807 | MaterialPageRoute( | |
264bf286 | 808 | builder: (context) => const ProxmoxGeneralSettingsForm())), |
7c043135 | 809 | child: const Text('Settings'), |
0e468546 TM |
810 | ) |
811 | ], | |
812 | ); | |
813 | } | |
814 | } | |
e2515b17 DC |
815 | |
816 | Uri normalizeUrl(String urlText) { | |
817 | if (urlText.startsWith('https://')) { | |
818 | urlText = urlText.substring('https://'.length); | |
819 | } | |
820 | if (urlText.startsWith('http://')) { | |
31126444 | 821 | throw Exception("HTTP without TLS is not supported"); |
e2515b17 DC |
822 | } |
823 | ||
824 | return Uri.https(urlText, ''); | |
825 | } |