[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