[pve-devel] [PATCH manager v2 2/3] add node/ACME.js

Dominik Csapak d.csapak at proxmox.com
Fri May 4 11:53:34 CEST 2018


this provides the grid for editing domains for letsencrypt,
order/renew the certificates, and the window for creating an
ACME account

Signed-off-by: Dominik Csapak <d.csapak at proxmox.com>
---
 www/manager6/Makefile     |   1 +
 www/manager6/Parser.js    |  29 ++++
 www/manager6/node/ACME.js | 427 ++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 457 insertions(+)
 create mode 100644 www/manager6/node/ACME.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 60e8103e..c29824bf 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -97,6 +97,7 @@ JSSRC= 				                 	\
 	node/StatusView.js				\
 	node/Summary.js					\
 	node/Subscription.js				\
+	node/ACME.js					\
 	node/Config.js					\
 	window/Migrate.js				\
 	window/BulkAction.js				\
diff --git a/www/manager6/Parser.js b/www/manager6/Parser.js
index 8253bd80..13dce766 100644
--- a/www/manager6/Parser.js
+++ b/www/manager6/Parser.js
@@ -5,6 +5,35 @@ Ext.define('PVE.Parser', { statics: {
 
     // this class only contains static functions
 
+    parseACME: function(value) {
+	if (!value) {
+	    return;
+	}
+
+	var res = {};
+	var errors = false;
+
+	Ext.Array.each(value.split(','), function(p) {
+	    if (!p || p.match(/^\s*$/)) {
+		return; //continue
+	    }
+
+	    var match_res;
+	    if ((match_res = p.match(/^(?:domains=)?((?:[a-zA-Z0-9\-\.]+[;, ]?)+)$/)) !== null) {
+		res.domains = match_res[1].split(/[;, ]/);
+	    } else {
+		errors = true;
+		return false;
+	    }
+	});
+
+	if (errors || !res) {
+	    return;
+	}
+
+	return res;
+    },
+
     parseBoolean: function(value, default_value) {
 	if (!Ext.isDefined(value)) {
 	    return default_value;
diff --git a/www/manager6/node/ACME.js b/www/manager6/node/ACME.js
new file mode 100644
index 00000000..febb16e1
--- /dev/null
+++ b/www/manager6/node/ACME.js
@@ -0,0 +1,427 @@
+Ext.define('PVE.node.ACMEEditor', {
+    extend: 'Proxmox.window.Edit',
+    xtype: 'pveACMEEditor',
+
+    subject: gettext('Domains'),
+    items: [
+	{
+	    xtype: 'inputpanel',
+	    items: [
+		{
+		    xtype: 'textarea',
+		    fieldLabel: gettext('Domains'),
+		    emptyText: "domain1.example.com\ndomain2.example.com",
+		    name: 'domains'
+		}
+	    ],
+	    onGetValues: function(values) {
+		if (!values.domains) {
+		    return {
+			'delete': 'acme'
+		    };
+		}
+		var domains = values.domains.split(/\n/).join(';');
+		return {
+		    'acme': 'domains=' + domains
+		};
+	    }
+	}
+    ],
+
+    initComponent: function() {
+	var me = this;
+	me.callParent();
+
+	me.load({
+	    success: function(response, opts) {
+		var res = PVE.Parser.parseACME(response.result.data.acme);
+		if (res) {
+		    res.domains = res.domains.join(' ');
+		    me.setValues(res);
+		}
+	    }
+	});
+    }
+});
+
+Ext.define('PVE.node.ACMEAccountCreate', {
+    extend: 'Proxmox.window.Edit',
+
+    width: 400,
+    title: gettext('Register Account'),
+    isCreate: true,
+    method: 'POST',
+    submitText: gettext('Register'),
+    url: '/cluster/acme/account',
+    showTaskViewer: true,
+
+    items: [
+	{
+	    xtype: 'proxmoxComboGrid',
+	    name: 'directory',
+	    allowBlank: false,
+	    valueField: 'url',
+	    displayField: 'name',
+	    fieldLabel: gettext('ACME Directory'),
+	    store: {
+		autoLoad: true,
+		fields: ['name', 'url'],
+		idProperty: ['name'],
+		proxy: {
+		    type: 'proxmox',
+		    url: '/api2/json/cluster/acme/directories'
+		},
+		sorters: {
+		    property: 'name',
+		    order: 'ASC'
+		}
+	    },
+	    listConfig: {
+		columns: [
+		    {
+			header: gettext('Name'),
+			dataIndex: 'name',
+			flex: 1
+		    },
+		    {
+			header: gettext('URL'),
+			dataIndex: 'url',
+			flex: 1
+		    }
+		]
+	    },
+	    listeners: {
+		change: function(combogrid, value) {
+		    var me = this;
+		    if (!value) {
+			return;
+		    }
+
+		    var disp = me.up('window').down('#tos_url_display');
+		    var field = me.up('window').down('#tos_url');
+		    var checkbox = me.up('window').down('#tos_checkbox');
+
+		    disp.setValue(gettext('Loading'));
+		    field.setValue(undefined);
+		    checkbox.setValue(undefined);
+
+		    Proxmox.Utils.API2Request({
+			url: '/cluster/acme/tos',
+			method: 'GET',
+			params: {
+			    directory: value
+			},
+			success: function(response, opt) {
+			    me.up('window').down('#tos_url').setValue(response.result.data);
+			    me.up('window').down('#tos_url_display').setValue(response.result.data);
+			},
+			failure: function(response, opt) {
+			    Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+			}
+		    });
+		}
+	    }
+	},
+	{
+	    xtype: 'displayfield',
+	    itemId: 'tos_url_display',
+	    fieldLabel: gettext('Terms of Service'),
+	    renderer: PVE.Utils.render_optional_url,
+	    name: 'tos_url_display'
+	},
+	{
+	    xtype: 'hidden',
+	    itemId: 'tos_url',
+	    name: 'tos_url'
+	},
+	{
+	    xtype: 'proxmoxcheckbox',
+	    itemId: 'tos_checkbox',
+	    fieldLabel: gettext('Accept TOS'),
+	    submitValue: false,
+	    validateValue: function(value) {
+		if (value && this.checked) {
+		    return true;
+		}
+		return false;
+	    }
+	},
+	{
+	    xtype: 'textfield',
+	    name: 'contact',
+	    vtype: 'email',
+	    allowBlank: false,
+	    fieldLabel: gettext('E-Mail')
+	}
+    ]
+
+});
+
+Ext.define('PVE.node.ACMEAccountView', {
+    extend: 'Proxmox.window.Edit',
+
+    width: 600,
+    fieldDefaults: {
+	labelWidth: 140
+    },
+
+    title: gettext('Account'),
+
+    items: [
+	{
+	    xtype: 'displayfield',
+	    fieldLabel: gettext('E-Mail'),
+	    name: 'email'
+	},
+	{
+	    xtype: 'displayfield',
+	    fieldLabel: gettext('Created'),
+	    name: 'createdAt'
+	},
+	{
+	    xtype: 'displayfield',
+	    fieldLabel: gettext('Status'),
+	    name: 'status'
+	},
+	{
+	    xtype: 'displayfield',
+	    fieldLabel: gettext('Directory'),
+	    renderer: PVE.Utils.render_optional_url,
+	    name: 'directory'
+	},
+	{
+	    xtype: 'displayfield',
+	    fieldLabel: gettext('Terms of Services'),
+	    renderer: PVE.Utils.render_optional_url,
+	    name: 'tos'
+	}
+    ],
+
+    initComponent: function() {
+	var me = this;
+
+	if (!me.accountname) {
+	    throw "no account name defined";
+	}
+
+	me.url = '/cluster/acme/account/' + me.accountname;
+
+	me.callParent();
+
+	// hide OK/Reset button, because we just want to show data
+	me.down('toolbar[dock=bottom]').setVisible(false);
+
+	me.load({
+	    success: function(response) {
+		var data = response.result.data;
+		data.email = data.account.contact[0];
+		data.createdAt = data.account.createdAt;
+		data.status = data.account.status;
+		me.setValues(data);
+	    }
+	});
+    }
+});
+
+Ext.define('PVE.node.ACME', {
+    extend: 'Proxmox.grid.ObjectGrid',
+    xtype: 'pveACMEView',
+
+    margin: '10 0 0 0',
+    title: 'ACME',
+
+    tbar: [
+	{
+	    xtype: 'button',
+	    itemId: 'edit',
+	    text: gettext('Edit Domains'),
+	    handler: function() {
+		this.up('grid').run_editor();
+	    }
+	},
+	{
+	    xtype: 'button',
+	    itemId: 'createaccount',
+	    text: gettext('Register Account'),
+	    handler: function() {
+		var me = this.up('grid');
+		var win = Ext.create('PVE.node.ACMEAccountCreate', {
+		    taskDone: function() {
+			me.load_account();
+			me.reload();
+		    }
+		});
+		win.show();
+	    }
+	},
+	{
+	    xtype: 'button',
+	    itemId: 'viewaccount',
+	    text: gettext('View Account'),
+	    handler: function() {
+		var me = this.up('grid');
+		var win = Ext.create('PVE.node.ACMEAccountView', {
+		    accountname: 'default'
+		});
+		win.show();
+	    }
+	},
+	{
+	    xtype: 'button',
+	    itemId: 'order',
+	    text: gettext('Order Certificate'),
+	    handler: function() {
+		var me = this.up('grid');
+
+		Proxmox.Utils.API2Request({
+		    method: 'POST',
+		    params: {
+			force: 1
+		    },
+		    url: '/nodes/' + me.nodename + '/certificates/acme/certificate',
+		    success: function(response, opt) {
+			var win = Ext.create('Proxmox.window.TaskViewer', {
+			    upid: response.result.data,
+			    taskDone: function(success) {
+				me.certificate_order_finished(success);
+			    }
+			});
+			win.show();
+		    },
+		    failure: function(response, opt) {
+			Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+		    }
+		});
+	    }
+	},
+	{
+	    xtype: 'proxmoxButton',
+	    itemId: 'renew',
+	    text: gettext('Renew Certificate'),
+	    selModel: null,
+	    confirmMsg: gettext("Are you sure you want to force renew the certificate?"),
+	    handler: function() {
+		var me = this.up('grid');
+		Proxmox.Utils.API2Request({
+		    method: 'PUT',
+		    params: {
+			force: 1
+		    },
+		    url: '/nodes/' + me.nodename + '/certificates/acme/certificate',
+		    success: function(response, opt) {
+			var win = Ext.create('Proxmox.window.TaskViewer', {
+			    upid: response.result.data,
+			    taskDone: function(success) {
+				me.certificate_order_finished(success);
+			    }
+			});
+			win.show();
+		    },
+		    failure: function(response, opt) {
+			Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+		    }
+		});
+	    }
+	}
+    ],
+
+    certificate_order_finished: function(success) {
+	if (!success) {
+	    return;
+	}
+	var txt = gettext('pveproxy will be restarted with new certificates, please reload the GUI!');
+	Ext.getBody().mask(txt, ['pve-static-mask']);
+	// reload after 10 seconds automatically
+	Ext.defer(function() {
+	    window.location.reload(true);
+	}, 10000);
+    },
+
+    set_button_status: function() {
+	var me = this;
+
+	var account = !!me.account;
+	var acmeObj = PVE.Parser.parseACME(me.getObjectValue('acme'));
+	var domains = acmeObj ? acmeObj.domains.length : 0;
+
+	var order = me.down('#order');
+	var renew = me.down('#renew');
+	order.setVisible(account);
+	order.setDisabled(!account || !domains);
+	renew.setVisible(account);
+	renew.setDisabled(!account || !domains);
+
+
+	me.down('#createaccount').setVisible(!account);
+	me.down('#viewaccount').setVisible(account);
+    },
+
+    load_account: function() {
+	var me = this;
+
+	// for now we only use the 'default' account
+	Proxmox.Utils.API2Request({
+	    url: '/cluster/acme/account/default',
+	    success: function(response, opt) {
+		me.account = response.result.data;
+		me.set_button_status();
+	    },
+	    failure: function(response, opt) {
+		me.account = undefined;
+		me.set_button_status();
+	    }
+	});
+    },
+
+    run_editor: function() {
+	var me = this;
+	var win = Ext.create(me.rows.acme.editor, me.editorConfig);
+	win.show();
+	win.on('destroy', me.reload, me);
+    },
+
+    listeners: {
+	itemdblclick: 'run_editor'
+    },
+
+    // account data gets loaded here
+    account: undefined,
+
+    disableSelection: true,
+
+    initComponent: function() {
+	var me = this;
+
+	if (!me.nodename) {
+	    throw "no nodename given";
+	}
+
+	me.url = '/api2/json/nodes/' + me.nodename + '/config';
+
+	me.editorConfig = {
+	    url: '/api2/extjs/nodes/' + me.nodename + '/config'
+	};
+	/*jslint confusion: true*/
+	/*acme is a string above*/
+	me.rows = {
+	    acme: {
+		defaultValue: '',
+		header: gettext('Domains'),
+		editor: 'PVE.node.ACMEEditor',
+		renderer: function(value) {
+		    var acmeObj = PVE.Parser.parseACME(value);
+		    if (acmeObj) {
+			return acmeObj.domains.join('<br>');
+		    }
+		    return Proxmox.Utils.noneText;
+		}
+	    }
+	};
+	/*jslint confusion: false*/
+
+	me.callParent();
+	me.mon(me.rstore, 'load', me.set_button_status, me);
+	me.rstore.startUpdate();
+	me.load_account();
+    }
+});
-- 
2.11.0





More information about the pve-devel mailing list