]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/node/ACME.js
ui: node disk manage: mark using ZFS reserved pool names as invalid
[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 direction: '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 editBtnText: (get) => get('accountEditable') ? gettext('Apply') : gettext('Edit'),
369 accountTextHidden: (get) => get('accountEditable') || !get('accountsAvailable'),
370 accountValueHidden: (get) => !get('accountEditable') || !get('accountsAvailable'),
371 },
372 },
373
374 controller: {
375 xclass: 'Ext.app.ViewController',
376
377 init: function(view) {
378 let accountSelector = this.lookup('accountselector');
379 accountSelector.store.on('load', this.onAccountsLoad, this);
380 },
381
382 onAccountsLoad: function(store, records, success) {
383 let me = this;
384 let vm = me.getViewModel();
385 let configaccount = vm.get('configaccount');
386 vm.set('accountsAvailable', records.length > 0);
387 if (me.autoChangeAccount && records.length > 0) {
388 me.changeAccount(records[0].data.name, () => {
389 vm.set('accountEditable', false);
390 me.reload();
391 });
392 me.autoChangeAccount = false;
393 } else if (configaccount) {
394 if (store.findExact('name', configaccount) !== -1) {
395 vm.set('account', configaccount);
396 } else {
397 vm.set('account', null);
398 }
399 }
400 },
401
402 addDomain: function() {
403 let me = this;
404 let view = me.getView();
405
406 Ext.create('PVE.node.ACMEDomainEdit', {
407 nodename: view.nodename,
408 nodeconfig: view.nodeconfig,
409 apiCallDone: function() {
410 me.reload();
411 },
412 }).show();
413 },
414
415 editDomain: function() {
416 let me = this;
417 let view = me.getView();
418
419 let selection = view.getSelection();
420 if (selection.length < 1) return;
421
422 Ext.create('PVE.node.ACMEDomainEdit', {
423 nodename: view.nodename,
424 nodeconfig: view.nodeconfig,
425 domain: selection[0].data,
426 apiCallDone: function() {
427 me.reload();
428 },
429 }).show();
430 },
431
432 removeDomain: function() {
433 let me = this;
434 let view = me.getView();
435 let selection = view.getSelection();
436 if (selection.length < 1) return;
437
438 let rec = selection[0].data;
439 let params = {};
440 if (rec.configkey !== 'acme') {
441 params.delete = rec.configkey;
442 } else {
443 let acme = PVE.Parser.parseACME(view.nodeconfig.acme);
444 PVE.Utils.remove_domain_from_acme(acme, rec.domain);
445 params.acme = PVE.Parser.printACME(acme);
446 }
447
448 Proxmox.Utils.API2Request({
449 method: 'PUT',
450 url: `/nodes/${view.nodename}/config`,
451 params,
452 success: function(response, opt) {
453 me.reload();
454 },
455 failure: function(response, opt) {
456 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
457 },
458 });
459 },
460
461 toggleEditAccount: function() {
462 let me = this;
463 let vm = me.getViewModel();
464 let editable = vm.get('accountEditable');
465 if (editable) {
466 me.changeAccount(vm.get('account'), function() {
467 vm.set('accountEditable', false);
468 me.reload();
469 });
470 } else {
471 vm.set('accountEditable', true);
472 }
473 },
474
475 changeAccount: function(account, callback) {
476 let me = this;
477 let view = me.getView();
478 let params = {};
479
480 let acme = PVE.Parser.parseACME(view.nodeconfig.acme);
481 acme.account = account;
482 params.acme = PVE.Parser.printACME(acme);
483
484 Proxmox.Utils.API2Request({
485 method: 'PUT',
486 waitMsgTarget: view,
487 url: `/nodes/${view.nodename}/config`,
488 params,
489 success: function(response, opt) {
490 if (Ext.isFunction(callback)) {
491 callback();
492 }
493 },
494 failure: function(response, opt) {
495 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
496 },
497 });
498 },
499
500 order: function() {
501 let me = this;
502 let view = me.getView();
503
504 Proxmox.Utils.API2Request({
505 method: 'POST',
506 params: {
507 force: 1,
508 },
509 url: `/nodes/${view.nodename}/certificates/acme/certificate`,
510 success: function(response, opt) {
511 Ext.create('Proxmox.window.TaskViewer', {
512 upid: response.result.data,
513 taskDone: function(success) {
514 me.orderFinished(success);
515 },
516 }).show();
517 },
518 failure: function(response, opt) {
519 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
520 },
521 });
522 },
523
524 orderFinished: function(success) {
525 if (!success) return;
526 var txt = gettext('pveproxy will be restarted with new certificates, please reload the GUI!');
527 Ext.getBody().mask(txt, ['pve-static-mask']);
528 // reload after 10 seconds automatically
529 Ext.defer(function() {
530 window.location.reload(true);
531 }, 10000);
532 },
533
534 reload: function() {
535 let me = this;
536 let view = me.getView();
537 view.rstore.load();
538 },
539
540 addAccount: function() {
541 let me = this;
542 Ext.create('PVE.node.ACMEAccountCreate', {
543 autoShow: true,
544 taskDone: function() {
545 me.reload();
546 let accountSelector = me.lookup('accountselector');
547 me.autoChangeAccount = true;
548 accountSelector.store.load();
549 },
550 });
551 },
552 },
553
554 tbar: [
555 {
556 xtype: 'proxmoxButton',
557 text: gettext('Add'),
558 handler: 'addDomain',
559 selModel: false,
560 },
561 {
562 xtype: 'proxmoxButton',
563 text: gettext('Edit'),
564 disabled: true,
565 handler: 'editDomain',
566 },
567 {
568 xtype: 'proxmoxStdRemoveButton',
569 handler: 'removeDomain',
570 },
571 '-',
572 {
573 xtype: 'button',
574 reference: 'order',
575 text: gettext('Order Certificates Now'),
576 bind: {
577 disabled: '{!canOrder}',
578 },
579 handler: 'order',
580 },
581 '-',
582 {
583 xtype: 'displayfield',
584 value: gettext('Using Account') + ':',
585 bind: {
586 hidden: '{!accountsAvailable}',
587 },
588 },
589 {
590 xtype: 'displayfield',
591 reference: 'accounttext',
592 renderer: (val) => val || Proxmox.Utils.NoneText,
593 bind: {
594 value: '{account}',
595 hidden: '{accountTextHidden}',
596 },
597 },
598 {
599 xtype: 'pveACMEAccountSelector',
600 hidden: true,
601 reference: 'accountselector',
602 bind: {
603 value: '{account}',
604 hidden: '{accountValueHidden}',
605 },
606 },
607 {
608 xtype: 'button',
609 iconCls: 'fa black fa-pencil',
610 bind: {
611 iconCls: '{editBtnIcon}',
612 text: '{editBtnText}',
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 });