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