]> git.proxmox.com Git - pve-manager.git/blame - www/manager6/node/ACME.js
ui: acme add domain: prefill with nodename.
[pve-manager.git] / www / manager6 / node / ACME.js
CommitLineData
488be4c2
DC
1Ext.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: [
c0afd5cc
DC
13 {
14 xtype: 'proxmoxtextfield',
15 fieldLabel: gettext('Name'),
16 name: 'name',
17 emptyText: 'default',
18 allowBlank: true,
19 },
488be4c2
DC
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
121Ext.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
8e49a93f
DC
187Ext.define('PVE.node.ACMEDomainEdit', {
188 extend: 'Proxmox.window.Edit',
189 alias: 'widget.pveACMEDomainEdit',
190
191 subject: gettext('Domain'),
192 isCreate: false,
3c6b4c80 193 width: 450,
8e49a93f
DC
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;
6ac64c3a 209 let acmeObj = PVE.Parser.parseACME(nodeconfig.acme);
8e49a93f
DC
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',
3c6b4c80 247 fieldLabel: gettext('Challenge Type'),
8e49a93f 248 allowBlank: false,
3c6b4c80 249 value: 'standalone',
8e49a93f 250 comboItems: [
9c164224 251 ['standalone', 'HTTP'],
8e49a93f
DC
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');
a94b71fb
TL
278 let pluginField = view.down('field[name=plugin]');
279 pluginField.setDisabled(value !== 'dns');
280 pluginField.setHidden(value !== 'dns');
8e49a93f
DC
281 },
282 },
283 },
284 {
285 xtype: 'hidden',
286 name: 'alias',
287 },
8e49a93f
DC
288 {
289 xtype: 'pveACMEPluginSelector',
290 name: 'plugin',
291 disabled: true,
a94b71fb 292 hidden: true,
8e49a93f
DC
293 allowBlank: false,
294 },
a94b71fb
TL
295 {
296 xtype: 'proxmoxtextfield',
297 name: 'domain',
298 allowBlank: false,
299 vtype: 'DnsName',
300 value: '',
301 fieldLabel: gettext('Domain'),
302 },
8e49a93f
DC
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;
8b779b4a
TL
319 if (me.isCreate) {
320 me.domain = `${me.nodename}.`; // TODO: FQDN of node
321 }
8e49a93f
DC
322
323 me.url = `/api2/extjs/nodes/${me.nodename}/config`;
324
325 me.callParent();
326
327 if (!me.isCreate) {
328 me.setValues(me.domain);
8b779b4a
TL
329 } else {
330 me.setValues({ domain: me.domain });
8e49a93f
DC
331 }
332 },
333});
334
fd254233
DC
335Ext.define('pve-acme-domains', {
336 extend: 'Ext.data.Model',
337 fields: ['domain', 'type', 'alias', 'plugin', 'configkey'],
338 idProperty: 'domain',
339});
340
488be4c2 341Ext.define('PVE.node.ACME', {
fd254233
DC
342 extend: 'Ext.grid.Panel',
343 alias: 'widget.pveACMEView',
488be4c2
DC
344
345 margin: '10 0 0 0',
346 title: 'ACME',
347
fd254233
DC
348 viewModel: {
349 data: {
a8b6a80a 350 account: undefined,
fd254233 351 accountEditable: false,
a8b6a80a 352 accountsAvailable: false,
fd254233
DC
353 },
354
355 formulas: {
356 editBtnIcon: (get) => {
357 return 'fa black fa-' + (get('accountEditable') ? 'check' : 'pencil');
358 },
a8b6a80a
TL
359 accountTextHidden: (get) => get('accountEditable') || !get('accountsAvailable'),
360 accountValueHidden: (get) => !get('accountEditable') || !get('accountsAvailable'),
fd254233
DC
361 },
362 },
363
364 controller: {
365 xclass: 'Ext.app.ViewController',
366
a8b6a80a
TL
367 init: function(view) {
368 let vm = this.getViewModel();
369 let accountSelector = this.lookup('accountselector');
370 accountSelector.store.on('load', this.onAccountsLoad, this);
371 },
372
373 onAccountsLoad: function(store, records, success) {
374 let vm = this.getViewModel();
375 vm.set('accountsAvailable', records.length > 0);
376 },
377
fd254233
DC
378 addDomain: function() {
379 let me = this;
380 let view = me.getView();
381
382 Ext.create('PVE.node.ACMEDomainEdit', {
383 nodename: view.nodename,
384 nodeconfig: view.nodeconfig,
385 apiCallDone: function() {
386 me.reload();
387 },
388 }).show();
389 },
390
391 editDomain: function() {
392 let me = this;
393 let view = me.getView();
394
395 let selection = view.getSelection();
396 if (selection.length < 1) return;
397
398 Ext.create('PVE.node.ACMEDomainEdit', {
399 nodename: view.nodename,
400 nodeconfig: view.nodeconfig,
401 domain: selection[0].data,
402 apiCallDone: function() {
403 me.reload();
404 },
405 }).show();
406 },
407
408 removeDomain: function() {
409 let me = this;
410 let view = me.getView();
411 let selection = view.getSelection();
412 if (selection.length < 1) return;
413
414 let rec = selection[0].data;
415 let params = {};
416 if (rec.configkey !== 'acme') {
417 params.delete = rec.configkey;
418 } else {
419 let acme = PVE.Parser.parseACME(view.nodeconfig.acme);
420 PVE.Utils.remove_domain_from_acme(acme, rec.domain);
421 params.acme = PVE.Parser.printACME(acme);
422 }
423
424 Proxmox.Utils.API2Request({
425 method: 'PUT',
426 url: `/nodes/${view.nodename}/config`,
427 params,
428 success: function(response, opt) {
429 me.reload();
430 },
431 failure: function(response, opt) {
432 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
433 },
434 });
435 },
436
437 toggleEditAccount: function() {
438 let me = this;
439 let vm = me.getViewModel();
440 let editable = vm.get('accountEditable');
441 if (editable) {
442 me.changeAccount(vm.get('account'), function() {
443 vm.set('accountEditable', false);
444 me.reload();
445 });
446 } else {
447 vm.set('accountEditable', true);
448 }
449 },
450
451 changeAccount: function(account, callback) {
452 let me = this;
453 let view = me.getView();
454 let params = {};
455
456 let acme = PVE.Parser.parseACME(view.nodeconfig.acme);
457 acme.account = account;
458 params.acme = PVE.Parser.printACME(acme);
459
460 Proxmox.Utils.API2Request({
461 method: 'PUT',
462 waitMsgTarget: view,
463 url: `/nodes/${view.nodename}/config`,
464 params,
465 success: function(response, opt) {
466 if (Ext.isFunction(callback)) {
467 callback();
468 }
469 },
470 failure: function(response, opt) {
471 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
472 },
473 });
474 },
475
476 order: function() {
477 let me = this;
478 let view = me.getView();
479
480 Proxmox.Utils.API2Request({
481 method: 'POST',
482 params: {
483 force: 1,
484 },
485 url: `/nodes/${view.nodename}/certificates/acme/certificate`,
486 success: function(response, opt) {
487 Ext.create('Proxmox.window.TaskViewer', {
488 upid: response.result.data,
489 taskDone: function(success) {
490 me.orderFinished(success);
491 },
492 }).show();
493 },
494 failure: function(response, opt) {
495 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
496 },
497 });
498 },
499
500 orderFinished: function(success) {
501 if (!success) return;
502 var txt = gettext('pveproxy will be restarted with new certificates, please reload the GUI!');
503 Ext.getBody().mask(txt, ['pve-static-mask']);
504 // reload after 10 seconds automatically
505 Ext.defer(function() {
506 window.location.reload(true);
507 }, 10000);
508 },
509
510 reload: function() {
511 let me = this;
512 let view = me.getView();
513 view.rstore.load();
514 },
515
516 gotoAccounts: function() {
517 let sp = Ext.state.Manager.getProvider();
518 sp.set('dctab', { value: 'acme' }, true);
519 Ext.ComponentQuery.query('pveResourceTree')[0].selectById('root');
520 },
521 },
522
488be4c2
DC
523 tbar: [
524 {
fd254233
DC
525 xtype: 'proxmoxButton',
526 text: gettext('Add'),
527 handler: 'addDomain',
528 selModel: false,
529 },
530 {
531 xtype: 'proxmoxButton',
532 text: gettext('Edit'),
533 disabled: true,
534 handler: 'editDomain',
535 },
536 {
537 xtype: 'proxmoxStdRemoveButton',
538 handler: 'removeDomain',
488be4c2 539 },
fd254233 540 '-',
488be4c2
DC
541 {
542 xtype: 'button',
fd254233 543 reference: 'order',
a8b6a80a 544 text: gettext('Order Certificates Now'),
fd254233
DC
545 handler: 'order',
546 },
547 '-',
548 {
549 xtype: 'displayfield',
a8b6a80a
TL
550 value: gettext('Using Account') + ':',
551 bind: {
552 hidden: '{!accountsAvailable}',
553 },
fd254233
DC
554 },
555 {
556 xtype: 'displayfield',
557 reference: 'accounttext',
558 bind: {
559 value: '{account}',
a8b6a80a 560 hidden: '{accountTextHidden}',
fd254233
DC
561 },
562 },
563 {
564 xtype: 'pveACMEAccountSelector',
565 hidden: true,
566 reference: 'accountselector',
567 bind: {
568 value: '{account}',
a8b6a80a 569 hidden: '{accountValueHidden}',
fd254233 570 },
488be4c2
DC
571 },
572 {
573 xtype: 'button',
fd254233
DC
574 iconCls: 'fa black fa-pencil',
575 baseCls: 'x-plain',
576 userCls: 'pointer',
577 bind: {
a8b6a80a
TL
578 iconCls: '{editBtnIcon}',
579 hidden: '{!accountsAvailable}',
fd254233
DC
580 },
581 handler: 'toggleEditAccount',
488be4c2 582 },
a8b6a80a
TL
583 {
584 xtype: 'displayfield',
585 value: gettext('No Account available.'),
586 bind: {
587 hidden: '{accountsAvailable}',
588 },
589 },
488be4c2
DC
590 {
591 xtype: 'button',
fd254233
DC
592 hidden: true,
593 reference: 'accountlink',
594 text: gettext('Go to ACME Accounts'),
a8b6a80a
TL
595 bind: {
596 hidden: '{accountsAvailable}',
597 },
fd254233 598 handler: 'gotoAccounts',
488be4c2
DC
599 }
600 ],
601
fd254233
DC
602 updateStore: function(store, records, success) {
603 let me = this;
604 let data = [];
605 let rec;
606 if (success && records.length > 0) {
607 rec = records[0];
608 } else {
609 rec = {
610 data: {}
611 };
488be4c2 612 }
488be4c2 613
fd254233 614 me.nodeconfig = rec.data; // save nodeconfig for updates
488be4c2 615
fd254233 616 let account = 'default';
488be4c2 617
fd254233
DC
618 if (rec.data.acme) {
619 let obj = PVE.Parser.parseACME(rec.data.acme);
620 (obj.domains || []).forEach(domain => {
621 if (domain === '') return;
622 let record = {
623 domain,
624 type: 'standalone',
625 configkey: 'acme',
626 };
627 data.push(record);
628 });
488be4c2 629
fd254233
DC
630 if (obj.account) {
631 account = obj.account;
632 }
633 }
488be4c2 634
fd254233
DC
635 let accounttext = me.lookup('accounttext');
636 let vm = me.getViewModel();
637 let oldaccount = vm.get('account');
638
639 // account changed, and we do not edit currently, load again to verify
640 if (oldaccount !== account && !vm.get('accountEditable')) {
641 Proxmox.Utils.API2Request({
642 url: `/cluster/acme/account/${account}`,
643 waitMsgTarget: me,
644 success: function(response, opt) {
645 vm.set('account', account);
646 },
647 failure: function(response, opt) {
648 vm.set('account', Proxmox.Utils.NoneText);
649 },
650 });
651 }
488be4c2 652
fd254233
DC
653 for (let i = 0; i < PVE.Utils.acmedomain_count; i++) {
654 let acmedomain = rec.data[`acmedomain${i}`];
655 if (!acmedomain) continue;
488be4c2 656
fd254233
DC
657 let record = PVE.Parser.parsePropertyString(acmedomain, 'domain');
658 record.type = 'dns';
659 record.configkey = `acmedomain${i}`;
660 data.push(record);
661 }
662
663 me.store.loadData(data, false);
488be4c2
DC
664 },
665
666 listeners: {
fd254233 667 itemdblclick: 'editDomain',
488be4c2
DC
668 },
669
fd254233
DC
670 columns: [
671 {
672 dataIndex: 'domain',
3c6b4c80 673 flex: 5,
fd254233
DC
674 text: gettext('Domain'),
675 },
676 {
677 dataIndex: 'type',
3c6b4c80 678 flex: 1,
fd254233
DC
679 text: gettext('Type'),
680 },
681 {
682 dataIndex: 'plugin',
3c6b4c80 683 flex: 1,
fd254233
DC
684 text: gettext('Plugin'),
685 },
686 ],
488be4c2
DC
687
688 initComponent: function() {
689 var me = this;
690
691 if (!me.nodename) {
692 throw "no nodename given";
693 }
694
fd254233 695 me.rstore = Ext.create('Proxmox.data.UpdateStore', {
1b90cfc6 696 interval: 10 * 1000,
fd254233
DC
697 autoStart: true,
698 storeid: `pve-node-domains-${me.nodename}`,
699 proxy: {
700 type: 'proxmox',
701 url: `/api2/json/nodes/${me.nodename}/config`,
702 },
703 });
704
705 me.store = Ext.create('Ext.data.Store', {
706 model: 'pve-acme-domains',
707 sorters: 'domain',
708 });
488be4c2
DC
709
710 me.callParent();
fd254233
DC
711 me.mon(me.rstore, 'load', 'updateStore', me);
712 Proxmox.Utils.monStoreErrors(me, me.rstore);
713 },
488be4c2 714});