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