]> git.proxmox.com Git - pve-manager.git/blame - www/manager6/node/ACME.js
ui: guest import: add warning for losing efi state
[pve-manager.git] / www / manager6 / node / ACME.js
CommitLineData
488be4c2
DC
1Ext.define('PVE.node.ACMEAccountCreate', {
2 extend: 'Proxmox.window.Edit',
fc40915c 3 mixins: ['Proxmox.Mixin.CBind'],
488be4c2 4
04a8058e 5 width: 450,
488be4c2
DC
6 title: gettext('Register Account'),
7 isCreate: true,
8 method: 'POST',
9 submitText: gettext('Register'),
10 url: '/cluster/acme/account',
11 showTaskViewer: true,
fc40915c 12 defaultExists: false,
488be4c2
DC
13
14 items: [
c0afd5cc
DC
15 {
16 xtype: 'proxmoxtextfield',
e023535e 17 fieldLabel: gettext('Account Name'),
c0afd5cc 18 name: 'name',
fc40915c
DC
19 cbind: {
20 emptyText: (get) => get('defaultExists') ? '' : 'default',
21 allowBlank: (get) => !get('defaultExists'),
22 },
c0afd5cc 23 },
04a8058e
DC
24 {
25 xtype: 'textfield',
26 name: 'contact',
27 vtype: 'email',
28 allowBlank: false,
f6710aac 29 fieldLabel: gettext('E-Mail'),
04a8058e 30 },
488be4c2
DC
31 {
32 xtype: 'proxmoxComboGrid',
33 name: 'directory',
34 allowBlank: false,
35 valueField: 'url',
36 displayField: 'name',
37 fieldLabel: gettext('ACME Directory'),
38 store: {
39 autoLoad: true,
40 fields: ['name', 'url'],
41 idProperty: ['name'],
42 proxy: {
43 type: 'proxmox',
f6710aac 44 url: '/api2/json/cluster/acme/directories',
488be4c2
DC
45 },
46 sorters: {
47 property: 'name',
392e3cf1 48 direction: 'ASC',
f6710aac 49 },
488be4c2
DC
50 },
51 listConfig: {
52 columns: [
53 {
54 header: gettext('Name'),
55 dataIndex: 'name',
f6710aac 56 flex: 1,
488be4c2
DC
57 },
58 {
59 header: gettext('URL'),
60 dataIndex: 'url',
f6710aac
TL
61 flex: 1,
62 },
63 ],
488be4c2
DC
64 },
65 listeners: {
66 change: function(combogrid, value) {
67 var me = this;
68 if (!value) {
69 return;
70 }
71
72 var disp = me.up('window').down('#tos_url_display');
73 var field = me.up('window').down('#tos_url');
74 var checkbox = me.up('window').down('#tos_checkbox');
75
76 disp.setValue(gettext('Loading'));
77 field.setValue(undefined);
78 checkbox.setValue(undefined);
1d5c5ba1 79 checkbox.setHidden(true);
488be4c2
DC
80
81 Proxmox.Utils.API2Request({
7fb70c3b 82 url: '/cluster/acme/meta',
488be4c2
DC
83 method: 'GET',
84 params: {
f6710aac 85 directory: value,
488be4c2
DC
86 },
87 success: function(response, opt) {
7fb70c3b
FG
88 if (response.result.data.termsOfService) {
89 field.setValue(response.result.data.termsOfService);
90 disp.setValue(response.result.data.termsOfService);
91 checkbox.setHidden(false);
92 } else {
93 disp.setValue(undefined);
94 }
488be4c2
DC
95 },
96 failure: function(response, opt) {
97 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
f6710aac 98 },
488be4c2 99 });
f6710aac
TL
100 },
101 },
488be4c2
DC
102 },
103 {
104 xtype: 'displayfield',
105 itemId: 'tos_url_display',
488be4c2 106 renderer: PVE.Utils.render_optional_url,
f6710aac 107 name: 'tos_url_display',
488be4c2
DC
108 },
109 {
110 xtype: 'hidden',
111 itemId: 'tos_url',
f6710aac 112 name: 'tos_url',
488be4c2
DC
113 },
114 {
115 xtype: 'proxmoxcheckbox',
116 itemId: 'tos_checkbox',
04a8058e 117 boxLabel: gettext('Accept TOS'),
488be4c2
DC
118 submitValue: false,
119 validateValue: function(value) {
120 if (value && this.checked) {
121 return true;
122 }
123 return false;
f6710aac 124 },
488be4c2 125 },
f6710aac 126 ],
488be4c2
DC
127
128});
129
130Ext.define('PVE.node.ACMEAccountView', {
131 extend: 'Proxmox.window.Edit',
132
133 width: 600,
134 fieldDefaults: {
f6710aac 135 labelWidth: 140,
488be4c2
DC
136 },
137
138 title: gettext('Account'),
139
140 items: [
141 {
142 xtype: 'displayfield',
143 fieldLabel: gettext('E-Mail'),
f6710aac 144 name: 'email',
488be4c2
DC
145 },
146 {
147 xtype: 'displayfield',
148 fieldLabel: gettext('Created'),
f6710aac 149 name: 'createdAt',
488be4c2
DC
150 },
151 {
152 xtype: 'displayfield',
153 fieldLabel: gettext('Status'),
f6710aac 154 name: 'status',
488be4c2
DC
155 },
156 {
157 xtype: 'displayfield',
158 fieldLabel: gettext('Directory'),
159 renderer: PVE.Utils.render_optional_url,
f6710aac 160 name: 'directory',
488be4c2
DC
161 },
162 {
163 xtype: 'displayfield',
164 fieldLabel: gettext('Terms of Services'),
165 renderer: PVE.Utils.render_optional_url,
f6710aac
TL
166 name: 'tos',
167 },
488be4c2
DC
168 ],
169
170 initComponent: function() {
171 var me = this;
172
173 if (!me.accountname) {
174 throw "no account name defined";
175 }
176
177 me.url = '/cluster/acme/account/' + me.accountname;
178
179 me.callParent();
180
181 // hide OK/Reset button, because we just want to show data
182 me.down('toolbar[dock=bottom]').setVisible(false);
183
184 me.load({
185 success: function(response) {
186 var data = response.result.data;
187 data.email = data.account.contact[0];
188 data.createdAt = data.account.createdAt;
189 data.status = data.account.status;
190 me.setValues(data);
f6710aac 191 },
488be4c2 192 });
f6710aac 193 },
488be4c2
DC
194});
195
8e49a93f
DC
196Ext.define('PVE.node.ACMEDomainEdit', {
197 extend: 'Proxmox.window.Edit',
198 alias: 'widget.pveACMEDomainEdit',
199
200 subject: gettext('Domain'),
201 isCreate: false,
3c6b4c80 202 width: 450,
eff602d8 203 onlineHelp: 'sysadmin_certificate_management',
8e49a93f
DC
204
205 items: [
206 {
207 xtype: 'inputpanel',
208 onGetValues: function(values) {
209 let me = this;
210 let win = me.up('pveACMEDomainEdit');
211 let nodeconfig = win.nodeconfig;
212 let olddomain = win.domain || {};
213
214 let params = {
215 digest: nodeconfig.digest,
216 };
217
218 let configkey = olddomain.configkey;
6ac64c3a 219 let acmeObj = PVE.Parser.parseACME(nodeconfig.acme);
8e49a93f
DC
220
221 if (values.type === 'dns') {
222 if (!olddomain.configkey || olddomain.configkey === 'acme') {
223 // look for first free slot
224 for (let i = 0; i < PVE.Utils.acmedomain_count; i++) {
225 if (nodeconfig[`acmedomain${i}`] === undefined) {
226 configkey = `acmedomain${i}`;
227 break;
228 }
229 }
230 if (olddomain.domain) {
231 // we have to remove the domain from the acme domainlist
232 PVE.Utils.remove_domain_from_acme(acmeObj, olddomain.domain);
233 params.acme = PVE.Parser.printACME(acmeObj);
234 }
235 }
236
237 delete values.type;
238 params[configkey] = PVE.Parser.printPropertyString(values, 'domain');
239 } else {
240 if (olddomain.configkey && olddomain.configkey !== 'acme') {
241 // delete the old dns entry
242 params.delete = [olddomain.configkey];
243 }
244
245 // add new, remove old and make entries unique
246 PVE.Utils.add_domain_to_acme(acmeObj, values.domain);
247 PVE.Utils.remove_domain_from_acme(acmeObj, olddomain.domain);
248 params.acme = PVE.Parser.printACME(acmeObj);
249 }
250
251 return params;
252 },
253 items: [
254 {
255 xtype: 'proxmoxKVComboBox',
256 name: 'type',
3c6b4c80 257 fieldLabel: gettext('Challenge Type'),
8e49a93f 258 allowBlank: false,
3c6b4c80 259 value: 'standalone',
8e49a93f 260 comboItems: [
9c164224 261 ['standalone', 'HTTP'],
8e49a93f
DC
262 ['dns', 'DNS'],
263 ],
264 validator: function(value) {
265 let me = this;
266 let win = me.up('pveACMEDomainEdit');
267 let oldconfigkey = win.domain ? win.domain.configkey : undefined;
268 let val = me.getValue();
269 if (val === 'dns' && (!oldconfigkey || oldconfigkey === 'acme')) {
270 // we have to check if there is a 'acmedomain' slot left
271 let found = false;
272 for (let i = 0; i < PVE.Utils.acmedomain_count; i++) {
273 if (!win.nodeconfig[`acmedomain${i}`]) {
274 found = true;
275 }
276 }
277 if (!found) {
278 return gettext('Only 5 Domains with type DNS can be configured');
279 }
280 }
281
282 return true;
283 },
284 listeners: {
285 change: function(cb, value) {
286 let me = this;
287 let view = me.up('pveACMEDomainEdit');
a94b71fb
TL
288 let pluginField = view.down('field[name=plugin]');
289 pluginField.setDisabled(value !== 'dns');
290 pluginField.setHidden(value !== 'dns');
8e49a93f
DC
291 },
292 },
293 },
294 {
295 xtype: 'hidden',
296 name: 'alias',
297 },
8e49a93f
DC
298 {
299 xtype: 'pveACMEPluginSelector',
300 name: 'plugin',
301 disabled: true,
a94b71fb 302 hidden: true,
8e49a93f
DC
303 allowBlank: false,
304 },
a94b71fb
TL
305 {
306 xtype: 'proxmoxtextfield',
307 name: 'domain',
308 allowBlank: false,
309 vtype: 'DnsName',
310 value: '',
311 fieldLabel: gettext('Domain'),
312 },
8e49a93f
DC
313 ],
314 },
315 ],
316
317 initComponent: function() {
318 let me = this;
319
320 if (!me.nodename) {
321 throw 'no nodename given';
322 }
323
324 if (!me.nodeconfig) {
325 throw 'no nodeconfig given';
326 }
327
328 me.isCreate = !me.domain;
8b779b4a
TL
329 if (me.isCreate) {
330 me.domain = `${me.nodename}.`; // TODO: FQDN of node
331 }
8e49a93f
DC
332
333 me.url = `/api2/extjs/nodes/${me.nodename}/config`;
334
335 me.callParent();
336
337 if (!me.isCreate) {
338 me.setValues(me.domain);
8b779b4a
TL
339 } else {
340 me.setValues({ domain: me.domain });
8e49a93f
DC
341 }
342 },
343});
344
fd254233
DC
345Ext.define('pve-acme-domains', {
346 extend: 'Ext.data.Model',
347 fields: ['domain', 'type', 'alias', 'plugin', 'configkey'],
348 idProperty: 'domain',
349});
350
488be4c2 351Ext.define('PVE.node.ACME', {
fd254233
DC
352 extend: 'Ext.grid.Panel',
353 alias: 'widget.pveACMEView',
488be4c2
DC
354
355 margin: '10 0 0 0',
356 title: 'ACME',
357
e666f688
DC
358 emptyText: gettext('No Domains configured'),
359
fd254233
DC
360 viewModel: {
361 data: {
1580b605 362 domaincount: 0,
3071cc5b
DC
363 account: undefined, // the account we display
364 configaccount: undefined, // the account set in the config
fd254233 365 accountEditable: false,
a8b6a80a 366 accountsAvailable: false,
fd254233
DC
367 },
368
369 formulas: {
1580b605 370 canOrder: (get) => !!get('account') && get('domaincount') > 0,
80bd3209 371 editBtnIcon: (get) => 'fa black fa-' + (get('accountEditable') ? 'check' : 'pencil'),
8fc2d938 372 editBtnText: (get) => get('accountEditable') ? gettext('Apply') : gettext('Edit'),
a8b6a80a
TL
373 accountTextHidden: (get) => get('accountEditable') || !get('accountsAvailable'),
374 accountValueHidden: (get) => !get('accountEditable') || !get('accountsAvailable'),
fd254233
DC
375 },
376 },
377
378 controller: {
379 xclass: 'Ext.app.ViewController',
380
a8b6a80a 381 init: function(view) {
a8b6a80a
TL
382 let accountSelector = this.lookup('accountselector');
383 accountSelector.store.on('load', this.onAccountsLoad, this);
384 },
385
386 onAccountsLoad: function(store, records, success) {
80bd3209
DC
387 let me = this;
388 let vm = me.getViewModel();
3071cc5b 389 let configaccount = vm.get('configaccount');
a8b6a80a 390 vm.set('accountsAvailable', records.length > 0);
80bd3209
DC
391 if (me.autoChangeAccount && records.length > 0) {
392 me.changeAccount(records[0].data.name, () => {
31c9edc8 393 vm.set('accountEditable', false);
80bd3209 394 me.reload();
31c9edc8
TL
395 });
396 me.autoChangeAccount = false;
3071cc5b
DC
397 } else if (configaccount) {
398 if (store.findExact('name', configaccount) !== -1) {
399 vm.set('account', configaccount);
400 } else {
401 vm.set('account', null);
402 }
31c9edc8 403 }
a8b6a80a
TL
404 },
405
fd254233
DC
406 addDomain: function() {
407 let me = this;
408 let view = me.getView();
409
410 Ext.create('PVE.node.ACMEDomainEdit', {
411 nodename: view.nodename,
412 nodeconfig: view.nodeconfig,
413 apiCallDone: function() {
414 me.reload();
415 },
416 }).show();
417 },
418
419 editDomain: function() {
420 let me = this;
421 let view = me.getView();
422
423 let selection = view.getSelection();
424 if (selection.length < 1) return;
425
426 Ext.create('PVE.node.ACMEDomainEdit', {
427 nodename: view.nodename,
428 nodeconfig: view.nodeconfig,
429 domain: selection[0].data,
430 apiCallDone: function() {
431 me.reload();
432 },
433 }).show();
434 },
435
436 removeDomain: function() {
437 let me = this;
438 let view = me.getView();
439 let selection = view.getSelection();
440 if (selection.length < 1) return;
441
442 let rec = selection[0].data;
443 let params = {};
444 if (rec.configkey !== 'acme') {
445 params.delete = rec.configkey;
446 } else {
447 let acme = PVE.Parser.parseACME(view.nodeconfig.acme);
448 PVE.Utils.remove_domain_from_acme(acme, rec.domain);
449 params.acme = PVE.Parser.printACME(acme);
450 }
451
452 Proxmox.Utils.API2Request({
453 method: 'PUT',
454 url: `/nodes/${view.nodename}/config`,
455 params,
456 success: function(response, opt) {
457 me.reload();
458 },
459 failure: function(response, opt) {
460 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
461 },
462 });
463 },
464
465 toggleEditAccount: function() {
466 let me = this;
467 let vm = me.getViewModel();
468 let editable = vm.get('accountEditable');
469 if (editable) {
470 me.changeAccount(vm.get('account'), function() {
471 vm.set('accountEditable', false);
472 me.reload();
473 });
474 } else {
475 vm.set('accountEditable', true);
476 }
477 },
478
479 changeAccount: function(account, callback) {
480 let me = this;
481 let view = me.getView();
482 let params = {};
483
484 let acme = PVE.Parser.parseACME(view.nodeconfig.acme);
485 acme.account = account;
486 params.acme = PVE.Parser.printACME(acme);
487
488 Proxmox.Utils.API2Request({
489 method: 'PUT',
490 waitMsgTarget: view,
491 url: `/nodes/${view.nodename}/config`,
492 params,
493 success: function(response, opt) {
494 if (Ext.isFunction(callback)) {
495 callback();
496 }
497 },
498 failure: function(response, opt) {
499 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
500 },
501 });
502 },
503
504 order: function() {
505 let me = this;
506 let view = me.getView();
507
508 Proxmox.Utils.API2Request({
509 method: 'POST',
510 params: {
511 force: 1,
512 },
513 url: `/nodes/${view.nodename}/certificates/acme/certificate`,
514 success: function(response, opt) {
515 Ext.create('Proxmox.window.TaskViewer', {
516 upid: response.result.data,
517 taskDone: function(success) {
518 me.orderFinished(success);
519 },
520 }).show();
521 },
522 failure: function(response, opt) {
523 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
524 },
525 });
526 },
527
528 orderFinished: function(success) {
529 if (!success) return;
72cfb3d4
FS
530 // reload only if the Web UI is open on the same node that the cert was ordered for
531 if (this.getView().nodename !== Proxmox.NodeName) {
532 return;
533 }
fd254233
DC
534 var txt = gettext('pveproxy will be restarted with new certificates, please reload the GUI!');
535 Ext.getBody().mask(txt, ['pve-static-mask']);
536 // reload after 10 seconds automatically
537 Ext.defer(function() {
538 window.location.reload(true);
539 }, 10000);
540 },
541
542 reload: function() {
543 let me = this;
544 let view = me.getView();
545 view.rstore.load();
546 },
547
31c9edc8
TL
548 addAccount: function() {
549 let me = this;
550 Ext.create('PVE.node.ACMEAccountCreate', {
551 autoShow: true,
552 taskDone: function() {
553 me.reload();
554 let accountSelector = me.lookup('accountselector');
555 me.autoChangeAccount = true;
556 accountSelector.store.load();
557 },
558 });
fd254233
DC
559 },
560 },
561
488be4c2
DC
562 tbar: [
563 {
fd254233
DC
564 xtype: 'proxmoxButton',
565 text: gettext('Add'),
566 handler: 'addDomain',
567 selModel: false,
568 },
569 {
570 xtype: 'proxmoxButton',
571 text: gettext('Edit'),
572 disabled: true,
573 handler: 'editDomain',
574 },
575 {
576 xtype: 'proxmoxStdRemoveButton',
577 handler: 'removeDomain',
488be4c2 578 },
fd254233 579 '-',
488be4c2
DC
580 {
581 xtype: 'button',
fd254233 582 reference: 'order',
a8b6a80a 583 text: gettext('Order Certificates Now'),
31c9edc8 584 bind: {
1580b605 585 disabled: '{!canOrder}',
31c9edc8 586 },
fd254233
DC
587 handler: 'order',
588 },
589 '-',
590 {
591 xtype: 'displayfield',
a8b6a80a
TL
592 value: gettext('Using Account') + ':',
593 bind: {
594 hidden: '{!accountsAvailable}',
595 },
fd254233
DC
596 },
597 {
598 xtype: 'displayfield',
599 reference: 'accounttext',
3071cc5b 600 renderer: (val) => val || Proxmox.Utils.NoneText,
fd254233
DC
601 bind: {
602 value: '{account}',
a8b6a80a 603 hidden: '{accountTextHidden}',
fd254233
DC
604 },
605 },
606 {
607 xtype: 'pveACMEAccountSelector',
608 hidden: true,
609 reference: 'accountselector',
610 bind: {
611 value: '{account}',
a8b6a80a 612 hidden: '{accountValueHidden}',
fd254233 613 },
488be4c2
DC
614 },
615 {
616 xtype: 'button',
fd254233 617 iconCls: 'fa black fa-pencil',
fd254233 618 bind: {
a8b6a80a 619 iconCls: '{editBtnIcon}',
8fc2d938 620 text: '{editBtnText}',
a8b6a80a 621 hidden: '{!accountsAvailable}',
fd254233
DC
622 },
623 handler: 'toggleEditAccount',
488be4c2 624 },
a8b6a80a
TL
625 {
626 xtype: 'displayfield',
627 value: gettext('No Account available.'),
628 bind: {
629 hidden: '{accountsAvailable}',
630 },
631 },
488be4c2
DC
632 {
633 xtype: 'button',
fd254233
DC
634 hidden: true,
635 reference: 'accountlink',
31c9edc8 636 text: gettext('Add ACME Account'),
a8b6a80a
TL
637 bind: {
638 hidden: '{accountsAvailable}',
639 },
31c9edc8 640 handler: 'addAccount',
80bd3209 641 },
488be4c2
DC
642 ],
643
fd254233
DC
644 updateStore: function(store, records, success) {
645 let me = this;
646 let data = [];
647 let rec;
648 if (success && records.length > 0) {
649 rec = records[0];
650 } else {
651 rec = {
80bd3209 652 data: {},
fd254233 653 };
488be4c2 654 }
488be4c2 655
fd254233 656 me.nodeconfig = rec.data; // save nodeconfig for updates
488be4c2 657
fd254233 658 let account = 'default';
488be4c2 659
fd254233
DC
660 if (rec.data.acme) {
661 let obj = PVE.Parser.parseACME(rec.data.acme);
662 (obj.domains || []).forEach(domain => {
663 if (domain === '') return;
664 let record = {
665 domain,
666 type: 'standalone',
667 configkey: 'acme',
668 };
669 data.push(record);
670 });
488be4c2 671
fd254233
DC
672 if (obj.account) {
673 account = obj.account;
674 }
675 }
488be4c2 676
fd254233
DC
677 let vm = me.getViewModel();
678 let oldaccount = vm.get('account');
679
680 // account changed, and we do not edit currently, load again to verify
681 if (oldaccount !== account && !vm.get('accountEditable')) {
3071cc5b
DC
682 vm.set('configaccount', account);
683 me.lookup('accountselector').store.load();
fd254233 684 }
488be4c2 685
fd254233
DC
686 for (let i = 0; i < PVE.Utils.acmedomain_count; i++) {
687 let acmedomain = rec.data[`acmedomain${i}`];
688 if (!acmedomain) continue;
488be4c2 689
fd254233
DC
690 let record = PVE.Parser.parsePropertyString(acmedomain, 'domain');
691 record.type = 'dns';
692 record.configkey = `acmedomain${i}`;
693 data.push(record);
694 }
695
1580b605 696 vm.set('domaincount', data.length);
fd254233 697 me.store.loadData(data, false);
488be4c2
DC
698 },
699
700 listeners: {
fd254233 701 itemdblclick: 'editDomain',
488be4c2
DC
702 },
703
fd254233
DC
704 columns: [
705 {
706 dataIndex: 'domain',
3c6b4c80 707 flex: 5,
fd254233
DC
708 text: gettext('Domain'),
709 },
710 {
711 dataIndex: 'type',
3c6b4c80 712 flex: 1,
fd254233
DC
713 text: gettext('Type'),
714 },
715 {
716 dataIndex: 'plugin',
3c6b4c80 717 flex: 1,
fd254233
DC
718 text: gettext('Plugin'),
719 },
720 ],
488be4c2
DC
721
722 initComponent: function() {
723 var me = this;
724
725 if (!me.nodename) {
726 throw "no nodename given";
727 }
728
fd254233 729 me.rstore = Ext.create('Proxmox.data.UpdateStore', {
1b90cfc6 730 interval: 10 * 1000,
fd254233
DC
731 autoStart: true,
732 storeid: `pve-node-domains-${me.nodename}`,
733 proxy: {
734 type: 'proxmox',
735 url: `/api2/json/nodes/${me.nodename}/config`,
736 },
737 });
738
739 me.store = Ext.create('Ext.data.Store', {
740 model: 'pve-acme-domains',
741 sorters: 'domain',
742 });
488be4c2
DC
743
744 me.callParent();
fd254233
DC
745 me.mon(me.rstore, 'load', 'updateStore', me);
746 Proxmox.Utils.monStoreErrors(me, me.rstore);
1f249769 747 me.on('destroy', me.rstore.stopUpdate, me.rstore);
fd254233 748 },
488be4c2 749});