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