[pbs-devel] [PATCH proxmox-backup 14/16] gui: add API token UI

Fabian Grünbichler f.gruenbichler at proxmox.com
Wed Oct 28 12:37:15 CET 2020


Signed-off-by: Fabian Grünbichler <f.gruenbichler at proxmox.com>
---
 www/Makefile            |   2 +
 www/NavigationTree.js   |   6 ++
 www/Utils.js            |   8 ++
 www/config/TokenView.js | 218 ++++++++++++++++++++++++++++++++++++++++
 www/window/TokenEdit.js | 213 +++++++++++++++++++++++++++++++++++++++
 5 files changed, 447 insertions(+)
 create mode 100644 www/config/TokenView.js
 create mode 100644 www/window/TokenEdit.js

diff --git a/www/Makefile b/www/Makefile
index 75d389d9..ab056c8c 100644
--- a/www/Makefile
+++ b/www/Makefile
@@ -13,12 +13,14 @@ JSSRC=							\
 	data/RunningTasksStore.js			\
 	button/TaskButton.js				\
 	config/UserView.js				\
+	config/TokenView.js				\
 	config/RemoteView.js				\
 	config/ACLView.js				\
 	config/SyncView.js				\
 	config/VerifyView.js				\
 	window/UserEdit.js				\
 	window/UserPassword.js				\
+	window/TokenEdit.js				\
 	window/VerifyJobEdit.js				\
 	window/RemoteEdit.js				\
 	window/SyncJobEdit.js				\
diff --git a/www/NavigationTree.js b/www/NavigationTree.js
index 6524a5c3..d4e5d966 100644
--- a/www/NavigationTree.js
+++ b/www/NavigationTree.js
@@ -34,6 +34,12 @@ Ext.define('PBS.store.NavigationStore', {
 			path: 'pbsUserView',
 			leaf: true,
 		    },
+		    {
+			text: gettext('API Token'),
+			iconCls: 'fa fa-user-o',
+			path: 'pbsTokenView',
+			leaf: true,
+		    },
 		    {
 			text: gettext('Permissions'),
 			iconCls: 'fa fa-unlock',
diff --git a/www/Utils.js b/www/Utils.js
index 221a2f2b..58319345 100644
--- a/www/Utils.js
+++ b/www/Utils.js
@@ -84,6 +84,14 @@ Ext.define('PBS.Utils', {
 	return `Datastore ${what} ${id}`;
     },
 
+    extractTokenUser: function(tokenid) {
+	return tokenid.match(/^(.+)!([^!]+)$/)[1];
+    },
+
+    extractTokenName: function(tokenid) {
+	return tokenid.match(/^(.+)!([^!]+)$/)[2];
+    },
+
     constructor: function() {
 	var me = this;
 
diff --git a/www/config/TokenView.js b/www/config/TokenView.js
new file mode 100644
index 00000000..88b3f194
--- /dev/null
+++ b/www/config/TokenView.js
@@ -0,0 +1,218 @@
+Ext.define('pbs-tokens', {
+    extend: 'Ext.data.Model',
+    fields: [
+	'tokenid', 'tokenname', 'user', 'comment',
+	{ type: 'boolean', name: 'enable', defaultValue: true },
+	{ type: 'date', dateFormat: 'timestamp', name: 'expire' },
+    ],
+    idProperty: 'tokenid',
+});
+
+Ext.define('pbs-users-with-tokens', {
+    extend: 'Ext.data.Model',
+    fields: [
+	'userid', 'firstname', 'lastname', 'email', 'comment',
+	{ type: 'boolean', name: 'enable', defaultValue: true },
+	{ type: 'date', dateFormat: 'timestamp', name: 'expire' },
+	'tokens',
+    ],
+    idProperty: 'userid',
+    proxy: {
+	type: 'proxmox',
+	url: '/api2/json/access/users/?include_tokens=1',
+    },
+});
+
+Ext.define('PBS.config.TokenView', {
+    extend: 'Ext.grid.GridPanel',
+    alias: 'widget.pbsTokenView',
+
+    stateful: true,
+    stateId: 'grid-tokens',
+
+    title: gettext('API Tokens'),
+
+    controller: {
+	xclass: 'Ext.app.ViewController',
+
+	init: function(view) {
+	    view.userStore = Ext.create('Proxmox.data.UpdateStore', {
+		autoStart: true,
+		interval: 5 * 1000,
+		storeId: 'pbs-users-with-tokens',
+		storeid: 'pbs-users-with-tokens',
+		model: 'pbs-users-with-tokens',
+	    });
+	    view.userStore.on('load', this.onLoad, this);
+	    view.on('destroy', view.userStore.stopUpdate);
+	    Proxmox.Utils.monStoreErrors(view, view.userStore);
+	},
+
+	reload: function() { this.getView().userStore.load(); },
+
+	onLoad: function(store, data, success) {
+	    if (!success) return;
+
+	    let tokenStore = this.getView().store.rstore;
+
+	    let records = [];
+	    Ext.Array.each(data, function(user) {
+		let tokens = user.data.tokens || [];
+		Ext.Array.each(tokens, function(token) {
+		    let r = {};
+		    r.tokenid = token.tokenid;
+		    r.comment = token.comment;
+		    r.expire = token.expire;
+		    r.enable = token.enable;
+		    records.push(r);
+		});
+	    });
+
+	    tokenStore.loadData(records);
+	    tokenStore.fireEvent('load', tokenStore, records, true);
+	},
+
+	addToken: function() {
+	    let me = this;
+	    Ext.create('PBS.window.TokenEdit', {
+		isCreate: true,
+		listeners: {
+		    destroy: function() {
+			me.reload();
+		    },
+		},
+	    }).show();
+	},
+
+	editToken: function() {
+	    let me = this;
+	    let view = me.getView();
+	    let selection = view.getSelection();
+	    if (selection.length < 1) return;
+	    Ext.create('PBS.window.TokenEdit', {
+		user: PBS.Utils.extractTokenUser(selection[0].data.tokenid),
+		tokenname: PBS.Utils.extractTokenName(selection[0].data.tokenid),
+		listeners: {
+		    destroy: function() {
+			me.reload();
+		    },
+		},
+	    }).show();
+	},
+
+	showPermissions: function() {
+	    let me = this;
+	    let view = me.getView();
+	    let selection = view.getSelection();
+
+	    if (selection.length < 1) return;
+
+	    Ext.create('Proxmox.PermissionView', {
+		auth_id: selection[0].data.tokenid,
+		auth_id_name: 'auth_id',
+	    }).show();
+	},
+
+	renderUser: function(tokenid) {
+	    return Ext.String.htmlEncode(PBS.Utils.extractTokenUser(tokenid));
+	},
+
+	renderTokenname: function(tokenid) {
+	    return Ext.String.htmlEncode(PBS.Utils.extractTokenName(tokenid));
+	},
+
+    },
+
+    listeners: {
+	activate: 'reload',
+	itemdblclick: 'editToken',
+    },
+
+    store: {
+	type: 'diff',
+	autoDestroy: true,
+	autoDestroyRstore: true,
+	sorters: 'tokenid',
+	model: 'pbs-tokens',
+	rstore: {
+	    type: 'store',
+	    proxy: 'memory',
+	    storeid: 'pbs-tokens',
+	    model: 'pbs-tokens',
+	},
+    },
+
+    tbar: [
+	{
+	    xtype: 'proxmoxButton',
+	    text: gettext('Add'),
+	    handler: 'addToken',
+	    selModel: false,
+	},
+	{
+	    xtype: 'proxmoxButton',
+	    text: gettext('Edit'),
+	    handler: 'editToken',
+	    disabled: true,
+	},
+	{
+	    xtype: 'proxmoxStdRemoveButton',
+	    baseurl: '/access/users/',
+	    callback: 'reload',
+	    getUrl: function(rec) {
+		let tokenid = rec.getId();
+		let user = PBS.Utils.extractTokenUser(tokenid);
+		let tokenname = PBS.Utils.extractTokenName(tokenid);
+		return '/access/users/' + encodeURIComponent(user) + '/token/' + encodeURIComponent(tokenname);
+	    },
+	},
+	{
+	    xtype: 'proxmoxButton',
+	    text: gettext('Permissions'),
+	    handler: 'showPermissions',
+	    disabled: true,
+	},
+    ],
+
+    viewConfig: {
+	trackOver: false,
+    },
+
+    columns: [
+	{
+	    header: gettext('User'),
+	    width: 200,
+	    sortable: true,
+	    renderer: 'renderUser',
+	    dataIndex: 'tokenid',
+	},
+	{
+	    header: gettext('Token name'),
+	    width: 100,
+	    sortable: true,
+	    renderer: 'renderTokenname',
+	    dataIndex: 'tokenid',
+	},
+	{
+	    header: gettext('Enabled'),
+	    width: 80,
+	    sortable: true,
+	    renderer: Proxmox.Utils.format_boolean,
+	    dataIndex: 'enable',
+	},
+	{
+	    header: gettext('Expire'),
+	    width: 80,
+	    sortable: true,
+	    renderer: Proxmox.Utils.format_expire,
+	    dataIndex: 'expire',
+	},
+	{
+	    header: gettext('Comment'),
+	    sortable: false,
+	    renderer: Ext.String.htmlEncode,
+	    dataIndex: 'comment',
+	    flex: 1,
+	},
+    ],
+});
diff --git a/www/window/TokenEdit.js b/www/window/TokenEdit.js
new file mode 100644
index 00000000..6b41ae9d
--- /dev/null
+++ b/www/window/TokenEdit.js
@@ -0,0 +1,213 @@
+Ext.define('PBS.window.TokenEdit', {
+    extend: 'Proxmox.window.Edit',
+    alias: 'widget.pbsTokenEdit',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    onlineHelp: 'user_mgmt',
+
+    user: undefined,
+    tokenname: undefined,
+
+    isAdd: true,
+    isCreate: false,
+    fixedUser: false,
+
+    subject: gettext('API token'),
+
+    fieldDefaults: { labelWidth: 120 },
+
+    items: {
+	xtype: 'inputpanel',
+	column1: [
+	    {
+		xtype: 'pmxDisplayEditField',
+		cbind: {
+		    editable: (get) => get('isCreate') && !get('fixedUser'),
+		},
+		editConfig: {
+		    xtype: 'pbsUserSelector',
+		    allowBlank: false,
+		},
+		name: 'user',
+		value: Proxmox.UserName,
+		renderer: Ext.String.htmlEncode,
+		fieldLabel: gettext('User'),
+	    },
+	    {
+		xtype: 'pmxDisplayEditField',
+		cbind: {
+		    editable: '{isCreate}',
+		},
+		name: 'tokenname',
+		fieldLabel: gettext('Token Name'),
+		minLength: 2,
+		allowBlank: false,
+	    },
+	],
+
+	column2: [
+	    {
+                xtype: 'datefield',
+                name: 'expire',
+		emptyText: Proxmox.Utils.neverText,
+		format: 'Y-m-d',
+		submitFormat: 'U',
+                fieldLabel: gettext('Expire'),
+            },
+	    {
+		xtype: 'proxmoxcheckbox',
+		fieldLabel: gettext('Enabled'),
+		name: 'enable',
+		uncheckedValue: 0,
+		defaultValue: 1,
+		checked: true,
+	    },
+	],
+
+	columnB: [
+	    {
+		xtype: 'proxmoxtextfield',
+		name: 'comment',
+		fieldLabel: gettext('Comment'),
+	    },
+	],
+    },
+
+    getValues: function(dirtyOnly) {
+	var me = this;
+
+	var values = me.callParent(arguments);
+
+	// hack: ExtJS datefield does not submit 0, so we need to set that
+	if (!values.expire) {
+	    values.expire = 0;
+	}
+
+	if (me.isCreate) {
+	    me.url = '/api2/extjs/access/users/';
+	    let uid = encodeURIComponent(values.user);
+	    let tid = encodeURIComponent(values.tokenname);
+	    delete values.user;
+	    delete values.tokenname;
+
+	    me.url += `${uid}/token/${tid}`;
+	}
+
+	return values;
+    },
+
+    setValues: function(values) {
+	var me = this;
+
+	if (Ext.isDefined(values.expire)) {
+	    if (values.expire) {
+		values.expire = new Date(values.expire * 1000);
+	    } else {
+		// display 'never' instead of '1970-01-01'
+		values.expire = null;
+	    }
+	}
+
+	me.callParent([values]);
+    },
+
+    initComponent: function() {
+	let me = this;
+
+	me.url = '/api2/extjs/access/users/';
+
+	me.callParent();
+
+	if (me.isCreate) {
+	    me.method = 'POST';
+	} else {
+	    me.method = 'PUT';
+
+	    let uid = encodeURIComponent(me.user);
+	    let tid = encodeURIComponent(me.tokenname);
+
+	    me.url += `${uid}/token/${tid}`;
+	    me.load({
+		success: function(response, options) {
+		    let values = response.result.data;
+		    values.user = me.user;
+		    values.tokenname = me.tokenname;
+		    me.setValues(values);
+		},
+	    });
+	}
+    },
+
+    apiCallDone: function(success, response, options) {
+	let res = response.result.data;
+	if (!success || !res || !res.value) {
+	    return;
+	}
+
+	Ext.create('PBS.window.TokenShow', {
+	    autoShow: true,
+	    tokenid: res.tokenid,
+	    secret: res.value,
+	});
+    },
+});
+
+Ext.define('PBS.window.TokenShow', {
+    extend: 'Ext.window.Window',
+    alias: ['widget.pbsTokenShow'],
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    width: 600,
+    modal: true,
+    resizable: false,
+    title: gettext('Token Secret'),
+
+    items: [
+	{
+	    xtype: 'container',
+	    layout: 'form',
+	    bodyPadding: 10,
+	    border: false,
+	    fieldDefaults: {
+		labelWidth: 100,
+		anchor: '100%',
+            },
+	    padding: '0 10 10 10',
+	    items: [
+		{
+		    xtype: 'textfield',
+		    fieldLabel: gettext('Token ID'),
+		    cbind: {
+			value: '{tokenid}',
+		    },
+		    editable: false,
+		},
+		{
+		    xtype: 'textfield',
+		    fieldLabel: gettext('Secret'),
+		    inputId: 'token-secret-value',
+		    cbind: {
+			value: '{secret}',
+		    },
+		    editable: false,
+		},
+	    ],
+	},
+	{
+	    xtype: 'component',
+	    border: false,
+	    padding: '10 10 10 10',
+	    userCls: 'pmx-hint',
+	    html: gettext('Please record the API token secret - it will only be displayed now'),
+	},
+    ],
+    buttons: [
+	{
+	    handler: function(b) {
+		document.getElementById('token-secret-value').select();
+		document.execCommand("copy");
+	    },
+	    text: gettext('Copy Secret Value'),
+	},
+    ],
+});
-- 
2.20.1






More information about the pbs-devel mailing list