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