]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/node/ACME.js
ui: eslint: fix trailing comma and comma related whitespaces errors
[pve-manager.git] / www / manager6 / node / ACME.js
1 Ext.define('PVE.node.ACMEAccountCreate', {
2 extend: 'Proxmox.window.Edit',
3 mixins: ['Proxmox.Mixin.CBind'],
4
5 width: 450,
6 title: gettext('Register Account'),
7 isCreate: true,
8 method: 'POST',
9 submitText: gettext('Register'),
10 url: '/cluster/acme/account',
11 showTaskViewer: true,
12 defaultExists: false,
13
14 items: [
15 {
16 xtype: 'proxmoxtextfield',
17 fieldLabel: gettext('Account Name'),
18 name: 'name',
19 cbind: {
20 emptyText: (get) => get('defaultExists') ? '' : 'default',
21 allowBlank: (get) => !get('defaultExists'),
22 },
23 },
24 {
25 xtype: 'textfield',
26 name: 'contact',
27 vtype: 'email',
28 allowBlank: false,
29 fieldLabel: gettext('E-Mail'),
30 },
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',
44 url: '/api2/json/cluster/acme/directories',
45 },
46 sorters: {
47 property: 'name',
48 order: 'ASC',
49 },
50 },
51 listConfig: {
52 columns: [
53 {
54 header: gettext('Name'),
55 dataIndex: 'name',
56 flex: 1,
57 },
58 {
59 header: gettext('URL'),
60 dataIndex: 'url',
61 flex: 1,
62 },
63 ],
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);
79 checkbox.setHidden(true);
80
81 Proxmox.Utils.API2Request({
82 url: '/cluster/acme/tos',
83 method: 'GET',
84 params: {
85 directory: value,
86 },
87 success: function(response, opt) {
88 field.setValue(response.result.data);
89 disp.setValue(response.result.data);
90 checkbox.setHidden(false);
91 },
92 failure: function(response, opt) {
93 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
94 },
95 });
96 },
97 },
98 },
99 {
100 xtype: 'displayfield',
101 itemId: 'tos_url_display',
102 renderer: PVE.Utils.render_optional_url,
103 name: 'tos_url_display',
104 },
105 {
106 xtype: 'hidden',
107 itemId: 'tos_url',
108 name: 'tos_url',
109 },
110 {
111 xtype: 'proxmoxcheckbox',
112 itemId: 'tos_checkbox',
113 boxLabel: gettext('Accept TOS'),
114 submitValue: false,
115 validateValue: function(value) {
116 if (value && this.checked) {
117 return true;
118 }
119 return false;
120 },
121 },
122 ],
123
124 });
125
126 Ext.define('PVE.node.ACMEAccountView', {
127 extend: 'Proxmox.window.Edit',
128
129 width: 600,
130 fieldDefaults: {
131 labelWidth: 140,
132 },
133
134 title: gettext('Account'),
135
136 items: [
137 {
138 xtype: 'displayfield',
139 fieldLabel: gettext('E-Mail'),
140 name: 'email',
141 },
142 {
143 xtype: 'displayfield',
144 fieldLabel: gettext('Created'),
145 name: 'createdAt',
146 },
147 {
148 xtype: 'displayfield',
149 fieldLabel: gettext('Status'),
150 name: 'status',
151 },
152 {
153 xtype: 'displayfield',
154 fieldLabel: gettext('Directory'),
155 renderer: PVE.Utils.render_optional_url,
156 name: 'directory',
157 },
158 {
159 xtype: 'displayfield',
160 fieldLabel: gettext('Terms of Services'),
161 renderer: PVE.Utils.render_optional_url,
162 name: 'tos',
163 },
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);
187 },
188 });
189 },
190 });
191
192 Ext.define('PVE.node.ACMEDomainEdit', {
193 extend: 'Proxmox.window.Edit',
194 alias: 'widget.pveACMEDomainEdit',
195
196 subject: gettext('Domain'),
197 isCreate: false,
198 width: 450,
199 onlineHelp: 'sysadmin_certificate_management',
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;
215 let acmeObj = PVE.Parser.parseACME(nodeconfig.acme);
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',
253 fieldLabel: gettext('Challenge Type'),
254 allowBlank: false,
255 value: 'standalone',
256 comboItems: [
257 ['standalone', 'HTTP'],
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');
284 let pluginField = view.down('field[name=plugin]');
285 pluginField.setDisabled(value !== 'dns');
286 pluginField.setHidden(value !== 'dns');
287 },
288 },
289 },
290 {
291 xtype: 'hidden',
292 name: 'alias',
293 },
294 {
295 xtype: 'pveACMEPluginSelector',
296 name: 'plugin',
297 disabled: true,
298 hidden: true,
299 allowBlank: false,
300 },
301 {
302 xtype: 'proxmoxtextfield',
303 name: 'domain',
304 allowBlank: false,
305 vtype: 'DnsName',
306 value: '',
307 fieldLabel: gettext('Domain'),
308 },
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;
325 if (me.isCreate) {
326 me.domain = `${me.nodename}.`; // TODO: FQDN of node
327 }
328
329 me.url = `/api2/extjs/nodes/${me.nodename}/config`;
330
331 me.callParent();
332
333 if (!me.isCreate) {
334 me.setValues(me.domain);
335 } else {
336 me.setValues({ domain: me.domain });
337 }
338 },
339 });
340
341 Ext.define('pve-acme-domains', {
342 extend: 'Ext.data.Model',
343 fields: ['domain', 'type', 'alias', 'plugin', 'configkey'],
344 idProperty: 'domain',
345 });
346
347 Ext.define('PVE.node.ACME', {
348 extend: 'Ext.grid.Panel',
349 alias: 'widget.pveACMEView',
350
351 margin: '10 0 0 0',
352 title: 'ACME',
353
354 emptyText: gettext('No Domains configured'),
355
356 viewModel: {
357 data: {
358 domaincount: 0,
359 account: undefined, // the account we display
360 configaccount: undefined, // the account set in the config
361 accountEditable: false,
362 accountsAvailable: false,
363 },
364
365 formulas: {
366 canOrder: (get) => !!get('account') && get('domaincount') > 0,
367 editBtnIcon: (get) => 'fa black fa-' + (get('accountEditable') ? 'check' : 'pencil'),
368 accountTextHidden: (get) => get('accountEditable') || !get('accountsAvailable'),
369 accountValueHidden: (get) => !get('accountEditable') || !get('accountsAvailable'),
370 },
371 },
372
373 controller: {
374 xclass: 'Ext.app.ViewController',
375
376 init: function(view) {
377 let accountSelector = this.lookup('accountselector');
378 accountSelector.store.on('load', this.onAccountsLoad, this);
379 },
380
381 onAccountsLoad: function(store, records, success) {
382 let me = this;
383 let vm = me.getViewModel();
384 let configaccount = vm.get('configaccount');
385 vm.set('accountsAvailable', records.length > 0);
386 if (me.autoChangeAccount && records.length > 0) {
387 me.changeAccount(records[0].data.name, () => {
388 vm.set('accountEditable', false);
389 me.reload();
390 });
391 me.autoChangeAccount = false;
392 } else if (configaccount) {
393 if (store.findExact('name', configaccount) !== -1) {
394 vm.set('account', configaccount);
395 } else {
396 vm.set('account', null);
397 }
398 }
399 },
400
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
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 });
550 },
551 },
552
553 tbar: [
554 {
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',
569 },
570 '-',
571 {
572 xtype: 'button',
573 reference: 'order',
574 text: gettext('Order Certificates Now'),
575 bind: {
576 disabled: '{!canOrder}',
577 },
578 handler: 'order',
579 },
580 '-',
581 {
582 xtype: 'displayfield',
583 value: gettext('Using Account') + ':',
584 bind: {
585 hidden: '{!accountsAvailable}',
586 },
587 },
588 {
589 xtype: 'displayfield',
590 reference: 'accounttext',
591 renderer: (val) => val || Proxmox.Utils.NoneText,
592 bind: {
593 value: '{account}',
594 hidden: '{accountTextHidden}',
595 },
596 },
597 {
598 xtype: 'pveACMEAccountSelector',
599 hidden: true,
600 reference: 'accountselector',
601 bind: {
602 value: '{account}',
603 hidden: '{accountValueHidden}',
604 },
605 },
606 {
607 xtype: 'button',
608 iconCls: 'fa black fa-pencil',
609 baseCls: 'x-plain',
610 userCls: 'pointer',
611 bind: {
612 iconCls: '{editBtnIcon}',
613 hidden: '{!accountsAvailable}',
614 },
615 handler: 'toggleEditAccount',
616 },
617 {
618 xtype: 'displayfield',
619 value: gettext('No Account available.'),
620 bind: {
621 hidden: '{accountsAvailable}',
622 },
623 },
624 {
625 xtype: 'button',
626 hidden: true,
627 reference: 'accountlink',
628 text: gettext('Add ACME Account'),
629 bind: {
630 hidden: '{accountsAvailable}',
631 },
632 handler: 'addAccount',
633 },
634 ],
635
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 = {
644 data: {},
645 };
646 }
647
648 me.nodeconfig = rec.data; // save nodeconfig for updates
649
650 let account = 'default';
651
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 });
663
664 if (obj.account) {
665 account = obj.account;
666 }
667 }
668
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')) {
674 vm.set('configaccount', account);
675 me.lookup('accountselector').store.load();
676 }
677
678 for (let i = 0; i < PVE.Utils.acmedomain_count; i++) {
679 let acmedomain = rec.data[`acmedomain${i}`];
680 if (!acmedomain) continue;
681
682 let record = PVE.Parser.parsePropertyString(acmedomain, 'domain');
683 record.type = 'dns';
684 record.configkey = `acmedomain${i}`;
685 data.push(record);
686 }
687
688 vm.set('domaincount', data.length);
689 me.store.loadData(data, false);
690 },
691
692 listeners: {
693 itemdblclick: 'editDomain',
694 },
695
696 columns: [
697 {
698 dataIndex: 'domain',
699 flex: 5,
700 text: gettext('Domain'),
701 },
702 {
703 dataIndex: 'type',
704 flex: 1,
705 text: gettext('Type'),
706 },
707 {
708 dataIndex: 'plugin',
709 flex: 1,
710 text: gettext('Plugin'),
711 },
712 ],
713
714 initComponent: function() {
715 var me = this;
716
717 if (!me.nodename) {
718 throw "no nodename given";
719 }
720
721 me.rstore = Ext.create('Proxmox.data.UpdateStore', {
722 interval: 10 * 1000,
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 });
735
736 me.callParent();
737 me.mon(me.rstore, 'load', 'updateStore', me);
738 Proxmox.Utils.monStoreErrors(me, me.rstore);
739 me.on('destroy', me.rstore.stopUpdate, me.rstore);
740 },
741 });