]> git.proxmox.com Git - flutter/proxmox_login_manager.git/blame - lib/proxmox_login_form.dart
login form: factor out the ConnectionErrorDialog
[flutter/proxmox_login_manager.git] / lib / proxmox_login_form.dart
CommitLineData
0e468546 1import 'dart:io';
a9d1ee22 2import 'dart:async';
0e468546 3
62145778 4import 'package:biometric_storage/biometric_storage.dart';
0e468546 5import 'package:flutter/material.dart';
f0291f5a 6import 'package:collection/src/iterable_extensions.dart';
0e468546
TM
7import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart'
8 as proxclient;
9import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart';
10import 'package:proxmox_login_manager/proxmox_general_settings_form.dart';
11import 'package:proxmox_login_manager/proxmox_general_settings_model.dart';
12import 'package:proxmox_login_manager/proxmox_login_model.dart';
13import 'package:proxmox_login_manager/proxmox_tfa_form.dart';
1dfe0c76 14import 'package:proxmox_login_manager/extension.dart';
62145778 15import 'package:proxmox_login_manager/proxmox_password_store.dart';
0e468546
TM
16
17class 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
27class 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
33class 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
67class _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
189class 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
206class _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
634class 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
667class 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
690class 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
715class 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
753Uri 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}