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