www: use TFA widgets from widget toolkit
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
parent
52fbc86fc9
commit
9a7431e2e0
359
www/LoginView.js
359
www/LoginView.js
|
@ -105,7 +105,7 @@ Ext.define('PBS.LoginView', {
|
||||||
));
|
));
|
||||||
|
|
||||||
let resp = await new Promise((resolve, reject) => {
|
let resp = await new Promise((resolve, reject) => {
|
||||||
Ext.create('PBS.login.TfaWindow', {
|
Ext.create('Proxmox.window.TfaLoginWindow', {
|
||||||
userid,
|
userid,
|
||||||
ticket,
|
ticket,
|
||||||
challenge,
|
challenge,
|
||||||
|
@ -331,360 +331,3 @@ Ext.define('PBS.LoginView', {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
Ext.define('PBS.login.TfaWindow', {
|
|
||||||
extend: 'Ext.window.Window',
|
|
||||||
mixins: ['Proxmox.Mixin.CBind'],
|
|
||||||
|
|
||||||
title: gettext("Second login factor required"),
|
|
||||||
|
|
||||||
modal: true,
|
|
||||||
resizable: false,
|
|
||||||
width: 512,
|
|
||||||
layout: {
|
|
||||||
type: 'vbox',
|
|
||||||
align: 'stretch',
|
|
||||||
},
|
|
||||||
|
|
||||||
defaultButton: 'tfaButton',
|
|
||||||
|
|
||||||
viewModel: {
|
|
||||||
data: {
|
|
||||||
confirmText: gettext('Confirm Second Factor'),
|
|
||||||
canConfirm: false,
|
|
||||||
availableChallenge: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
cancelled: true,
|
|
||||||
|
|
||||||
controller: {
|
|
||||||
xclass: 'Ext.app.ViewController',
|
|
||||||
|
|
||||||
init: function(view) {
|
|
||||||
let me = this;
|
|
||||||
let vm = me.getViewModel();
|
|
||||||
|
|
||||||
if (!view.userid) {
|
|
||||||
throw "no userid given";
|
|
||||||
}
|
|
||||||
if (!view.ticket) {
|
|
||||||
throw "no ticket given";
|
|
||||||
}
|
|
||||||
const challenge = view.challenge;
|
|
||||||
if (!challenge) {
|
|
||||||
throw "no challenge given";
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastTabId = me.getLastTabUsed();
|
|
||||||
let initialTab = -1, i = 0;
|
|
||||||
for (const k of ['webauthn', 'totp', 'recovery']) {
|
|
||||||
const available = !!challenge[k];
|
|
||||||
vm.set(`availableChallenge.${k}`, available);
|
|
||||||
|
|
||||||
if (available) {
|
|
||||||
if (i === lastTabId) {
|
|
||||||
initialTab = i;
|
|
||||||
} else if (initialTab < 0) {
|
|
||||||
initialTab = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
view.down('tabpanel').setActiveTab(initialTab);
|
|
||||||
|
|
||||||
if (challenge.recovery) {
|
|
||||||
me.lookup('availableRecovery').update(Ext.String.htmlEncode(
|
|
||||||
gettext('Available recovery keys: ') + view.challenge.recovery.join(', '),
|
|
||||||
));
|
|
||||||
me.lookup('availableRecovery').setVisible(true);
|
|
||||||
if (view.challenge.recovery.length <= 3) {
|
|
||||||
me.lookup('recoveryLow').setVisible(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (challenge.webauthn && initialTab === 0) {
|
|
||||||
let _promise = me.loginWebauthn();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
control: {
|
|
||||||
'tabpanel': {
|
|
||||||
tabchange: function(tabPanel, newCard, oldCard) {
|
|
||||||
// for now every TFA method has at max one field, so keep it simple..
|
|
||||||
let oldField = oldCard.down('field');
|
|
||||||
if (oldField) {
|
|
||||||
oldField.setDisabled(true);
|
|
||||||
}
|
|
||||||
let newField = newCard.down('field');
|
|
||||||
if (newField) {
|
|
||||||
newField.setDisabled(false);
|
|
||||||
newField.focus();
|
|
||||||
newField.validate();
|
|
||||||
}
|
|
||||||
|
|
||||||
let confirmText = newCard.confirmText || gettext('Confirm Second Factor');
|
|
||||||
this.getViewModel().set('confirmText', confirmText);
|
|
||||||
|
|
||||||
this.saveLastTabUsed(tabPanel, newCard);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'field': {
|
|
||||||
validitychange: function(field, valid) {
|
|
||||||
// triggers only for enabled fields and we disable the one from the
|
|
||||||
// non-visible tab, so we can just directly use the valid param
|
|
||||||
this.getViewModel().set('canConfirm', valid);
|
|
||||||
},
|
|
||||||
afterrender: field => field.focus(), // ensure focus after initial render
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
saveLastTabUsed: function(tabPanel, card) {
|
|
||||||
let id = tabPanel.items.indexOf(card);
|
|
||||||
window.localStorage.setItem('PBS.TFALogin.lastTab', JSON.stringify({ id }));
|
|
||||||
},
|
|
||||||
|
|
||||||
getLastTabUsed: function() {
|
|
||||||
let data = window.localStorage.getItem('PBS.TFALogin.lastTab');
|
|
||||||
if (typeof data === 'string') {
|
|
||||||
let last = JSON.parse(data);
|
|
||||||
return last.id;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
onClose: function() {
|
|
||||||
let me = this;
|
|
||||||
let view = me.getView();
|
|
||||||
|
|
||||||
if (!view.cancelled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
view.onReject();
|
|
||||||
},
|
|
||||||
|
|
||||||
cancel: function() {
|
|
||||||
this.getView().close();
|
|
||||||
},
|
|
||||||
|
|
||||||
loginTotp: function() {
|
|
||||||
let me = this;
|
|
||||||
|
|
||||||
let code = me.lookup('totp').getValue();
|
|
||||||
let _promise = me.finishChallenge(`totp:${code}`);
|
|
||||||
},
|
|
||||||
|
|
||||||
loginWebauthn: async function() {
|
|
||||||
let me = this;
|
|
||||||
let view = me.getView();
|
|
||||||
|
|
||||||
me.lookup('webAuthnWaiting').setVisible(true);
|
|
||||||
me.lookup('webAuthnError').setVisible(false);
|
|
||||||
|
|
||||||
let challenge = view.challenge.webauthn;
|
|
||||||
|
|
||||||
if (typeof challenge.string !== 'string') {
|
|
||||||
// Byte array fixup, keep challenge string:
|
|
||||||
challenge.string = challenge.publicKey.challenge;
|
|
||||||
challenge.publicKey.challenge = PBS.Utils.base64url_to_bytes(challenge.string);
|
|
||||||
for (const cred of challenge.publicKey.allowCredentials) {
|
|
||||||
cred.id = PBS.Utils.base64url_to_bytes(cred.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let controller = new AbortController();
|
|
||||||
challenge.signal = controller.signal;
|
|
||||||
|
|
||||||
let hwrsp;
|
|
||||||
try {
|
|
||||||
//Promise.race( ...
|
|
||||||
hwrsp = await navigator.credentials.get(challenge);
|
|
||||||
} catch (error) {
|
|
||||||
// we do NOT want to fail login because of canceling the challenge actively,
|
|
||||||
// in some browser that's the only way to switch over to another method as the
|
|
||||||
// disallow user input during the time the challenge is active
|
|
||||||
// checking for error.code === DOMException.ABORT_ERR only works in firefox -.-
|
|
||||||
this.getViewModel().set('canConfirm', true);
|
|
||||||
// FIXME: better handling, show some message, ...?
|
|
||||||
me.lookup('webAuthnError').setData({
|
|
||||||
error: Ext.htmlEncode(error.toString()),
|
|
||||||
});
|
|
||||||
me.lookup('webAuthnError').setVisible(true);
|
|
||||||
return;
|
|
||||||
} finally {
|
|
||||||
let waitingMessage = me.lookup('webAuthnWaiting');
|
|
||||||
if (waitingMessage) {
|
|
||||||
waitingMessage.setVisible(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = {
|
|
||||||
id: hwrsp.id,
|
|
||||||
type: hwrsp.type,
|
|
||||||
challenge: challenge.string,
|
|
||||||
rawId: PBS.Utils.bytes_to_base64url(hwrsp.rawId),
|
|
||||||
response: {
|
|
||||||
authenticatorData: PBS.Utils.bytes_to_base64url(
|
|
||||||
hwrsp.response.authenticatorData,
|
|
||||||
),
|
|
||||||
clientDataJSON: PBS.Utils.bytes_to_base64url(hwrsp.response.clientDataJSON),
|
|
||||||
signature: PBS.Utils.bytes_to_base64url(hwrsp.response.signature),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await me.finishChallenge("webauthn:" + JSON.stringify(response));
|
|
||||||
},
|
|
||||||
|
|
||||||
loginRecovery: function() {
|
|
||||||
let me = this;
|
|
||||||
|
|
||||||
let key = me.lookup('recoveryKey').getValue();
|
|
||||||
let _promise = me.finishChallenge(`recovery:${key}`);
|
|
||||||
},
|
|
||||||
|
|
||||||
loginTFA: function() {
|
|
||||||
let me = this;
|
|
||||||
// avoid triggering more than once during challenge
|
|
||||||
me.getViewModel().set('canConfirm', false);
|
|
||||||
let view = me.getView();
|
|
||||||
let tfaPanel = view.down('tabpanel').getActiveTab();
|
|
||||||
me[tfaPanel.handler]();
|
|
||||||
},
|
|
||||||
|
|
||||||
finishChallenge: function(password) {
|
|
||||||
let me = this;
|
|
||||||
let view = me.getView();
|
|
||||||
view.cancelled = false;
|
|
||||||
|
|
||||||
let params = {
|
|
||||||
username: view.userid,
|
|
||||||
'tfa-challenge': view.ticket,
|
|
||||||
password,
|
|
||||||
};
|
|
||||||
|
|
||||||
let resolve = view.onResolve;
|
|
||||||
let reject = view.onReject;
|
|
||||||
view.close();
|
|
||||||
|
|
||||||
return Proxmox.Async.api2({
|
|
||||||
url: '/api2/extjs/access/ticket',
|
|
||||||
method: 'POST',
|
|
||||||
params,
|
|
||||||
})
|
|
||||||
.then(resolve)
|
|
||||||
.catch(reject);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
listeners: {
|
|
||||||
close: 'onClose',
|
|
||||||
},
|
|
||||||
|
|
||||||
items: [{
|
|
||||||
xtype: 'tabpanel',
|
|
||||||
region: 'center',
|
|
||||||
layout: 'fit',
|
|
||||||
bodyPadding: 10,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
xtype: 'panel',
|
|
||||||
title: 'WebAuthn',
|
|
||||||
iconCls: 'fa fa-fw fa-shield',
|
|
||||||
confirmText: gettext('Start WebAuthn challenge'),
|
|
||||||
handler: 'loginWebauthn',
|
|
||||||
bind: {
|
|
||||||
disabled: '{!availableChallenge.webauthn}',
|
|
||||||
},
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
xtype: 'box',
|
|
||||||
html: gettext('Please insert your authentication device and press its button'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'box',
|
|
||||||
html: gettext('Waiting for second factor.') +`<i class="fa fa-refresh fa-spin fa-fw"></i>`,
|
|
||||||
reference: 'webAuthnWaiting',
|
|
||||||
hidden: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'box',
|
|
||||||
data: {
|
|
||||||
error: '',
|
|
||||||
},
|
|
||||||
tpl: '<i class="fa fa-warning warning"></i> {error}',
|
|
||||||
reference: 'webAuthnError',
|
|
||||||
hidden: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'panel',
|
|
||||||
title: gettext('TOTP App'),
|
|
||||||
iconCls: 'fa fa-fw fa-clock-o',
|
|
||||||
handler: 'loginTotp',
|
|
||||||
bind: {
|
|
||||||
disabled: '{!availableChallenge.totp}',
|
|
||||||
},
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
xtype: 'textfield',
|
|
||||||
fieldLabel: gettext('Please enter your TOTP verification code'),
|
|
||||||
labelWidth: 300,
|
|
||||||
name: 'totp',
|
|
||||||
disabled: true,
|
|
||||||
reference: 'totp',
|
|
||||||
allowBlank: false,
|
|
||||||
regex: /^[0-9]{6}$/,
|
|
||||||
regexText: gettext('TOTP codes consist of six decimal digits'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'panel',
|
|
||||||
title: gettext('Recovery Key'),
|
|
||||||
iconCls: 'fa fa-fw fa-file-text-o',
|
|
||||||
handler: 'loginRecovery',
|
|
||||||
bind: {
|
|
||||||
disabled: '{!availableChallenge.recovery}',
|
|
||||||
},
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
xtype: 'box',
|
|
||||||
reference: 'availableRecovery',
|
|
||||||
hidden: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'textfield',
|
|
||||||
fieldLabel: gettext('Please enter one of your single-use recovery keys'),
|
|
||||||
labelWidth: 300,
|
|
||||||
name: 'recoveryKey',
|
|
||||||
disabled: true,
|
|
||||||
reference: 'recoveryKey',
|
|
||||||
allowBlank: false,
|
|
||||||
regex: /^[0-9a-f]{4}(-[0-9a-f]{4}){3}$/,
|
|
||||||
regexText: gettext('Does not look like a valid recovery key'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'box',
|
|
||||||
reference: 'recoveryLow',
|
|
||||||
hidden: true,
|
|
||||||
html: '<i class="fa fa-exclamation-triangle warning"></i>'
|
|
||||||
+ gettext('Less than {0} recovery keys available. Please generate a new set after login!'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}],
|
|
||||||
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
handler: 'loginTFA',
|
|
||||||
reference: 'tfaButton',
|
|
||||||
disabled: true,
|
|
||||||
bind: {
|
|
||||||
text: '{confirmText}',
|
|
||||||
disabled: '{!canConfirm}',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
|
@ -36,7 +36,6 @@ TAPE_UI_FILES= \
|
||||||
|
|
||||||
JSSRC= \
|
JSSRC= \
|
||||||
Utils.js \
|
Utils.js \
|
||||||
form/UserSelector.js \
|
|
||||||
form/TokenSelector.js \
|
form/TokenSelector.js \
|
||||||
form/AuthidSelector.js \
|
form/AuthidSelector.js \
|
||||||
form/RemoteSelector.js \
|
form/RemoteSelector.js \
|
||||||
|
@ -46,7 +45,6 @@ JSSRC= \
|
||||||
data/RunningTasksStore.js \
|
data/RunningTasksStore.js \
|
||||||
button/TaskButton.js \
|
button/TaskButton.js \
|
||||||
config/UserView.js \
|
config/UserView.js \
|
||||||
config/TfaView.js \
|
|
||||||
config/TokenView.js \
|
config/TokenView.js \
|
||||||
config/RemoteView.js \
|
config/RemoteView.js \
|
||||||
config/ACLView.js \
|
config/ACLView.js \
|
||||||
|
@ -56,9 +54,6 @@ JSSRC= \
|
||||||
config/CertificateView.js \
|
config/CertificateView.js \
|
||||||
config/NodeOptionView.js \
|
config/NodeOptionView.js \
|
||||||
window/ACLEdit.js \
|
window/ACLEdit.js \
|
||||||
window/AddTfaRecovery.js \
|
|
||||||
window/AddTotp.js \
|
|
||||||
window/AddWebauthn.js \
|
|
||||||
window/BackupFileDownloader.js \
|
window/BackupFileDownloader.js \
|
||||||
window/BackupGroupChangeOwner.js \
|
window/BackupGroupChangeOwner.js \
|
||||||
window/CreateDirectory.js \
|
window/CreateDirectory.js \
|
||||||
|
@ -71,7 +66,6 @@ JSSRC= \
|
||||||
window/UserPassword.js \
|
window/UserPassword.js \
|
||||||
window/Settings.js \
|
window/Settings.js \
|
||||||
window/TokenEdit.js \
|
window/TokenEdit.js \
|
||||||
window/TfaEdit.js \
|
|
||||||
window/VerifyJobEdit.js \
|
window/VerifyJobEdit.js \
|
||||||
window/ZFSCreate.js \
|
window/ZFSCreate.js \
|
||||||
dashboard/DataStoreStatistics.js \
|
dashboard/DataStoreStatistics.js \
|
||||||
|
|
|
@ -85,7 +85,7 @@ const proxmoxOnlineHelpInfo = {
|
||||||
},
|
},
|
||||||
"local-zfs-special-device": {
|
"local-zfs-special-device": {
|
||||||
"link": "/docs/sysadmin.html#local-zfs-special-device",
|
"link": "/docs/sysadmin.html#local-zfs-special-device",
|
||||||
"title": "ZFS Special Device"
|
"title": "ZFS special device"
|
||||||
},
|
},
|
||||||
"maintenance-pruning": {
|
"maintenance-pruning": {
|
||||||
"link": "/docs/maintenance.html#maintenance-pruning",
|
"link": "/docs/maintenance.html#maintenance-pruning",
|
||||||
|
@ -115,6 +115,10 @@ const proxmoxOnlineHelpInfo = {
|
||||||
"link": "/docs/network-management.html#sysadmin-network-configuration",
|
"link": "/docs/network-management.html#sysadmin-network-configuration",
|
||||||
"title": "Network Management"
|
"title": "Network Management"
|
||||||
},
|
},
|
||||||
|
"sysadmin-traffic-control": {
|
||||||
|
"link": "/docs/traffic-control.html#sysadmin-traffic-control",
|
||||||
|
"title": "Traffic Control"
|
||||||
|
},
|
||||||
"pve-integration": {
|
"pve-integration": {
|
||||||
"link": "/docs/pve-integration.html#pve-integration",
|
"link": "/docs/pve-integration.html#pve-integration",
|
||||||
"title": "`Proxmox VE`_ Integration"
|
"title": "`Proxmox VE`_ Integration"
|
||||||
|
@ -185,7 +189,7 @@ const proxmoxOnlineHelpInfo = {
|
||||||
},
|
},
|
||||||
"user-tfa": {
|
"user-tfa": {
|
||||||
"link": "/docs/user-management.html#user-tfa",
|
"link": "/docs/user-management.html#user-tfa",
|
||||||
"title": "Two-factor authentication"
|
"title": "Two-Factor Authentication"
|
||||||
},
|
},
|
||||||
"user-tfa-setup-totp": {
|
"user-tfa-setup-totp": {
|
||||||
"link": "/docs/user-management.html#user-tfa-setup-totp",
|
"link": "/docs/user-management.html#user-tfa-setup-totp",
|
||||||
|
|
|
@ -1,402 +0,0 @@
|
||||||
Ext.define('pbs-tfa-users', {
|
|
||||||
extend: 'Ext.data.Model',
|
|
||||||
fields: ['userid'],
|
|
||||||
idProperty: 'userid',
|
|
||||||
proxy: {
|
|
||||||
type: 'proxmox',
|
|
||||||
url: '/api2/json/access/tfa',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
Ext.define('pbs-tfa-entry', {
|
|
||||||
extend: 'Ext.data.Model',
|
|
||||||
fields: ['fullid', 'userid', 'type', 'description', 'created', 'enable'],
|
|
||||||
idProperty: 'fullid',
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
Ext.define('PBS.config.TfaView', {
|
|
||||||
extend: 'Ext.grid.GridPanel',
|
|
||||||
alias: 'widget.pbsTfaView',
|
|
||||||
|
|
||||||
title: gettext('Second Factors'),
|
|
||||||
reference: 'tfaview',
|
|
||||||
|
|
||||||
store: {
|
|
||||||
type: 'diff',
|
|
||||||
autoDestroy: true,
|
|
||||||
autoDestroyRstore: true,
|
|
||||||
model: 'pbs-tfa-entry',
|
|
||||||
rstore: {
|
|
||||||
type: 'store',
|
|
||||||
proxy: 'memory',
|
|
||||||
storeid: 'pbs-tfa-entry',
|
|
||||||
model: 'pbs-tfa-entry',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
controller: {
|
|
||||||
xclass: 'Ext.app.ViewController',
|
|
||||||
|
|
||||||
init: function(view) {
|
|
||||||
let me = this;
|
|
||||||
view.tfaStore = Ext.create('Proxmox.data.UpdateStore', {
|
|
||||||
autoStart: true,
|
|
||||||
interval: 5 * 1000,
|
|
||||||
storeid: 'pbs-tfa-users',
|
|
||||||
model: 'pbs-tfa-users',
|
|
||||||
});
|
|
||||||
view.tfaStore.on('load', this.onLoad, this);
|
|
||||||
view.on('destroy', view.tfaStore.stopUpdate);
|
|
||||||
Proxmox.Utils.monStoreErrors(view, view.tfaStore);
|
|
||||||
},
|
|
||||||
|
|
||||||
reload: function() { this.getView().tfaStore.load(); },
|
|
||||||
|
|
||||||
onLoad: function(store, data, success) {
|
|
||||||
if (!success) return;
|
|
||||||
|
|
||||||
let records = [];
|
|
||||||
Ext.Array.each(data, user => {
|
|
||||||
Ext.Array.each(user.data.entries, entry => {
|
|
||||||
records.push({
|
|
||||||
fullid: `${user.id}/${entry.id}`,
|
|
||||||
userid: user.id,
|
|
||||||
type: entry.type,
|
|
||||||
description: entry.description,
|
|
||||||
created: entry.created,
|
|
||||||
enable: entry.enable,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let rstore = this.getView().store.rstore;
|
|
||||||
rstore.loadData(records);
|
|
||||||
rstore.fireEvent('load', rstore, records, true);
|
|
||||||
},
|
|
||||||
|
|
||||||
addTotp: function() {
|
|
||||||
let me = this;
|
|
||||||
|
|
||||||
Ext.create('PBS.window.AddTotp', {
|
|
||||||
isCreate: true,
|
|
||||||
listeners: {
|
|
||||||
destroy: function() {
|
|
||||||
me.reload();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).show();
|
|
||||||
},
|
|
||||||
|
|
||||||
addWebauthn: function() {
|
|
||||||
let me = this;
|
|
||||||
|
|
||||||
Ext.create('PBS.window.AddWebauthn', {
|
|
||||||
isCreate: true,
|
|
||||||
listeners: {
|
|
||||||
destroy: function() {
|
|
||||||
me.reload();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).show();
|
|
||||||
},
|
|
||||||
|
|
||||||
addRecovery: async function() {
|
|
||||||
let me = this;
|
|
||||||
|
|
||||||
Ext.create('PBS.window.AddTfaRecovery', {
|
|
||||||
listeners: {
|
|
||||||
destroy: function() {
|
|
||||||
me.reload();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).show();
|
|
||||||
},
|
|
||||||
|
|
||||||
editItem: function() {
|
|
||||||
let me = this;
|
|
||||||
let view = me.getView();
|
|
||||||
let selection = view.getSelection();
|
|
||||||
if (selection.length !== 1 || selection[0].id.endsWith("/recovery")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ext.create('PBS.window.TfaEdit', {
|
|
||||||
'tfa-id': selection[0].data.fullid,
|
|
||||||
listeners: {
|
|
||||||
destroy: function() {
|
|
||||||
me.reload();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).show();
|
|
||||||
},
|
|
||||||
|
|
||||||
renderUser: fullid => fullid.split('/')[0],
|
|
||||||
|
|
||||||
renderEnabled: enabled => {
|
|
||||||
if (enabled === undefined) {
|
|
||||||
return Proxmox.Utils.yesText;
|
|
||||||
} else {
|
|
||||||
return Proxmox.Utils.format_boolean(enabled);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onRemoveButton: function(btn, event, record) {
|
|
||||||
let me = this;
|
|
||||||
|
|
||||||
Ext.create('PBS.tfa.confirmRemove', {
|
|
||||||
...record.data,
|
|
||||||
callback: password => me.removeItem(password, record),
|
|
||||||
})
|
|
||||||
.show();
|
|
||||||
},
|
|
||||||
|
|
||||||
removeItem: async function(password, record) {
|
|
||||||
let me = this;
|
|
||||||
|
|
||||||
let params = {};
|
|
||||||
if (password !== null) {
|
|
||||||
params.password = password;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
me.getView().mask(gettext('Please wait...'), 'x-mask-loading');
|
|
||||||
await Proxmox.Async.api2({
|
|
||||||
url: `/api2/extjs/access/tfa/${record.id}`,
|
|
||||||
method: 'DELETE',
|
|
||||||
params,
|
|
||||||
});
|
|
||||||
me.reload();
|
|
||||||
} catch (response) {
|
|
||||||
Ext.Msg.alert(gettext('Error'), response.result.message);
|
|
||||||
} finally {
|
|
||||||
me.getView().unmask();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
viewConfig: {
|
|
||||||
trackOver: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
listeners: {
|
|
||||||
itemdblclick: 'editItem',
|
|
||||||
},
|
|
||||||
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: gettext('User'),
|
|
||||||
width: 200,
|
|
||||||
sortable: true,
|
|
||||||
dataIndex: 'fullid',
|
|
||||||
renderer: 'renderUser',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: gettext('Enabled'),
|
|
||||||
width: 80,
|
|
||||||
sortable: true,
|
|
||||||
dataIndex: 'enable',
|
|
||||||
renderer: 'renderEnabled',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: gettext('TFA Type'),
|
|
||||||
width: 80,
|
|
||||||
sortable: true,
|
|
||||||
dataIndex: 'type',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: gettext('Created'),
|
|
||||||
width: 150,
|
|
||||||
sortable: true,
|
|
||||||
dataIndex: 'created',
|
|
||||||
renderer: Proxmox.Utils.render_timestamp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: gettext('Description'),
|
|
||||||
width: 300,
|
|
||||||
sortable: true,
|
|
||||||
dataIndex: 'description',
|
|
||||||
renderer: Ext.String.htmlEncode,
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
tbar: [
|
|
||||||
{
|
|
||||||
text: gettext('Add'),
|
|
||||||
menu: {
|
|
||||||
xtype: 'menu',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
text: gettext('TOTP'),
|
|
||||||
itemId: 'totp',
|
|
||||||
iconCls: 'fa fa-fw fa-clock-o',
|
|
||||||
handler: 'addTotp',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: gettext('Webauthn'),
|
|
||||||
itemId: 'webauthn',
|
|
||||||
iconCls: 'fa fa-fw fa-shield',
|
|
||||||
handler: 'addWebauthn',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: gettext('Recovery Keys'),
|
|
||||||
itemId: 'recovery',
|
|
||||||
iconCls: 'fa fa-fw fa-file-text-o',
|
|
||||||
handler: 'addRecovery',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'-',
|
|
||||||
{
|
|
||||||
xtype: 'proxmoxButton',
|
|
||||||
text: gettext('Edit'),
|
|
||||||
handler: 'editItem',
|
|
||||||
enableFn: rec => !rec.id.endsWith("/recovery"),
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'proxmoxButton',
|
|
||||||
disabled: true,
|
|
||||||
text: gettext('Remove'),
|
|
||||||
getRecordName: rec => rec.data.description,
|
|
||||||
handler: 'onRemoveButton',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
Ext.define('PBS.tfa.confirmRemove', {
|
|
||||||
extend: 'Proxmox.window.Edit',
|
|
||||||
mixins: ['Proxmox.Mixin.CBind'],
|
|
||||||
|
|
||||||
title: gettext("Confirm TFA Removal"),
|
|
||||||
|
|
||||||
modal: true,
|
|
||||||
resizable: false,
|
|
||||||
width: 600,
|
|
||||||
isCreate: true, // logic
|
|
||||||
isRemove: true,
|
|
||||||
|
|
||||||
url: '/access/tfa',
|
|
||||||
|
|
||||||
initComponent: function() {
|
|
||||||
let me = this;
|
|
||||||
|
|
||||||
if (typeof me.type !== "string") {
|
|
||||||
throw "missing type";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!me.callback) {
|
|
||||||
throw "missing callback";
|
|
||||||
}
|
|
||||||
|
|
||||||
me.callParent();
|
|
||||||
|
|
||||||
if (Proxmox.UserName === 'root@pam') {
|
|
||||||
me.lookup('password').setVisible(false);
|
|
||||||
me.lookup('password').setDisabled(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
submit: function() {
|
|
||||||
let me = this;
|
|
||||||
if (Proxmox.UserName === 'root@pam') {
|
|
||||||
me.callback(null);
|
|
||||||
} else {
|
|
||||||
me.callback(me.lookup('password').getValue());
|
|
||||||
}
|
|
||||||
me.close();
|
|
||||||
},
|
|
||||||
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
xtype: 'box',
|
|
||||||
padding: '0 0 10 0',
|
|
||||||
html: Ext.String.format(
|
|
||||||
gettext('Are you sure you want to remove this {0} entry?'),
|
|
||||||
'TFA',
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'container',
|
|
||||||
layout: {
|
|
||||||
type: 'hbox',
|
|
||||||
align: 'begin',
|
|
||||||
},
|
|
||||||
defaults: {
|
|
||||||
border: false,
|
|
||||||
layout: 'anchor',
|
|
||||||
flex: 1,
|
|
||||||
padding: 5,
|
|
||||||
},
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
xtype: 'container',
|
|
||||||
layout: {
|
|
||||||
type: 'vbox',
|
|
||||||
},
|
|
||||||
padding: '0 10 0 0',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
xtype: 'displayfield',
|
|
||||||
fieldLabel: gettext('User'),
|
|
||||||
cbind: {
|
|
||||||
value: '{userid}',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'displayfield',
|
|
||||||
fieldLabel: gettext('Type'),
|
|
||||||
cbind: {
|
|
||||||
value: '{type}',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'container',
|
|
||||||
layout: {
|
|
||||||
type: 'vbox',
|
|
||||||
},
|
|
||||||
padding: '0 0 0 10',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
xtype: 'displayfield',
|
|
||||||
fieldLabel: gettext('Created'),
|
|
||||||
renderer: v => Proxmox.Utils.render_timestamp(v),
|
|
||||||
cbind: {
|
|
||||||
value: '{created}',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'textfield',
|
|
||||||
fieldLabel: gettext('Description'),
|
|
||||||
cbind: {
|
|
||||||
value: '{description}',
|
|
||||||
},
|
|
||||||
emptyText: Proxmox.Utils.NoneText,
|
|
||||||
submitValue: false,
|
|
||||||
editable: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'textfield',
|
|
||||||
inputType: 'password',
|
|
||||||
fieldLabel: gettext('Password'),
|
|
||||||
minLength: 5,
|
|
||||||
reference: 'password',
|
|
||||||
name: 'password',
|
|
||||||
allowBlank: false,
|
|
||||||
validateBlank: true,
|
|
||||||
padding: '10 0 0 0',
|
|
||||||
cbind: {
|
|
||||||
emptyText: () =>
|
|
||||||
Ext.String.format(gettext("Confirm your ({0}) password"), Proxmox.UserName),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
|
@ -1,50 +0,0 @@
|
||||||
Ext.define('PBS.form.UserSelector', {
|
|
||||||
extend: 'Proxmox.form.ComboGrid',
|
|
||||||
alias: 'widget.pbsUserSelector',
|
|
||||||
|
|
||||||
allowBlank: false,
|
|
||||||
autoSelect: false,
|
|
||||||
valueField: 'userid',
|
|
||||||
displayField: 'userid',
|
|
||||||
|
|
||||||
editable: true,
|
|
||||||
anyMatch: true,
|
|
||||||
forceSelection: true,
|
|
||||||
|
|
||||||
store: {
|
|
||||||
model: 'pmx-users',
|
|
||||||
autoLoad: true,
|
|
||||||
params: {
|
|
||||||
enabled: 1,
|
|
||||||
},
|
|
||||||
sorters: 'userid',
|
|
||||||
},
|
|
||||||
|
|
||||||
listConfig: {
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: gettext('User'),
|
|
||||||
sortable: true,
|
|
||||||
dataIndex: 'userid',
|
|
||||||
renderer: Ext.String.htmlEncode,
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: gettext('Name'),
|
|
||||||
sortable: true,
|
|
||||||
renderer: (first, mD, rec) => Ext.String.htmlEncode(
|
|
||||||
`${first || ''} ${rec.data.lastname || ''}`,
|
|
||||||
),
|
|
||||||
dataIndex: 'firstname',
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: gettext('Comment'),
|
|
||||||
sortable: false,
|
|
||||||
dataIndex: 'comment',
|
|
||||||
renderer: Ext.String.htmlEncode,
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -20,7 +20,7 @@ Ext.define('PBS.AccessControlPanel', {
|
||||||
iconCls: 'fa fa-user',
|
iconCls: 'fa fa-user',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
xtype: 'pbsTfaView',
|
xtype: 'pmxTfaView',
|
||||||
title: gettext('Two Factor Authentication'),
|
title: gettext('Two Factor Authentication'),
|
||||||
itemId: 'tfa',
|
itemId: 'tfa',
|
||||||
iconCls: 'fa fa-key',
|
iconCls: 'fa fa-key',
|
||||||
|
|
|
@ -55,7 +55,7 @@ Ext.define('PBS.TapeManagement.TapeBackupWindow', {
|
||||||
fieldLabel: gettext('Eject Media'),
|
fieldLabel: gettext('Eject Media'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
xtype: 'pbsUserSelector',
|
xtype: 'pmxUserSelector',
|
||||||
name: 'notify-user',
|
name: 'notify-user',
|
||||||
fieldLabel: gettext('Notify User'),
|
fieldLabel: gettext('Notify User'),
|
||||||
emptyText: 'root@pam',
|
emptyText: 'root@pam',
|
||||||
|
|
|
@ -65,7 +65,7 @@ Ext.define('PBS.TapeManagement.BackupJobEdit', {
|
||||||
name: 'drive',
|
name: 'drive',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
xtype: 'pbsUserSelector',
|
xtype: 'pmxUserSelector',
|
||||||
name: 'notify-user',
|
name: 'notify-user',
|
||||||
fieldLabel: gettext('Notify User'),
|
fieldLabel: gettext('Notify User'),
|
||||||
emptyText: 'root@pam',
|
emptyText: 'root@pam',
|
||||||
|
|
|
@ -388,7 +388,7 @@ Ext.define('PBS.TapeManagement.TapeRestoreWindow', {
|
||||||
},
|
},
|
||||||
column1: [
|
column1: [
|
||||||
{
|
{
|
||||||
xtype: 'pbsUserSelector',
|
xtype: 'pmxUserSelector',
|
||||||
name: 'notify-user',
|
name: 'notify-user',
|
||||||
fieldLabel: gettext('Notify User'),
|
fieldLabel: gettext('Notify User'),
|
||||||
emptyText: gettext('Current User'),
|
emptyText: gettext('Current User'),
|
||||||
|
@ -398,7 +398,7 @@ Ext.define('PBS.TapeManagement.TapeRestoreWindow', {
|
||||||
renderer: Ext.String.htmlEncode,
|
renderer: Ext.String.htmlEncode,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
xtype: 'pbsUserSelector',
|
xtype: 'pmxUserSelector',
|
||||||
name: 'owner',
|
name: 'owner',
|
||||||
fieldLabel: gettext('Owner'),
|
fieldLabel: gettext('Owner'),
|
||||||
emptyText: gettext('Current User'),
|
emptyText: gettext('Current User'),
|
||||||
|
|
|
@ -34,7 +34,7 @@ Ext.define('PBS.window.ACLEdit', {
|
||||||
if (me.aclType === 'user') {
|
if (me.aclType === 'user') {
|
||||||
me.subject = gettext('User Permission');
|
me.subject = gettext('User Permission');
|
||||||
me.items.push({
|
me.items.push({
|
||||||
xtype: 'pbsUserSelector',
|
xtype: 'pmxUserSelector',
|
||||||
name: 'auth-id',
|
name: 'auth-id',
|
||||||
fieldLabel: gettext('User'),
|
fieldLabel: gettext('User'),
|
||||||
allowBlank: false,
|
allowBlank: false,
|
||||||
|
|
|
@ -1,224 +0,0 @@
|
||||||
Ext.define('PBS.window.AddTfaRecovery', {
|
|
||||||
extend: 'Proxmox.window.Edit',
|
|
||||||
alias: 'widget.pbsAddTfaRecovery',
|
|
||||||
mixins: ['Proxmox.Mixin.CBind'],
|
|
||||||
|
|
||||||
onlineHelp: 'user_mgmt',
|
|
||||||
isCreate: true,
|
|
||||||
isAdd: true,
|
|
||||||
subject: gettext('TFA recovery keys'),
|
|
||||||
width: 512,
|
|
||||||
method: 'POST',
|
|
||||||
|
|
||||||
fixedUser: false,
|
|
||||||
|
|
||||||
url: '/api2/extjs/access/tfa',
|
|
||||||
submitUrl: function(url, values) {
|
|
||||||
let userid = values.userid;
|
|
||||||
delete values.userid;
|
|
||||||
return `${url}/${userid}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
apiCallDone: function(success, response) {
|
|
||||||
if (!success) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let values = response
|
|
||||||
.result
|
|
||||||
.data
|
|
||||||
.recovery
|
|
||||||
.map((v, i) => `${i}: ${v}`)
|
|
||||||
.join("\n");
|
|
||||||
Ext.create('PBS.window.TfaRecoveryShow', {
|
|
||||||
autoShow: true,
|
|
||||||
userid: this.getViewModel().get('userid'),
|
|
||||||
values,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
viewModel: {
|
|
||||||
data: {
|
|
||||||
has_entry: false,
|
|
||||||
userid: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
controller: {
|
|
||||||
xclass: 'Ext.app.ViewController',
|
|
||||||
hasEntry: async function(userid) {
|
|
||||||
let me = this;
|
|
||||||
let view = me.getView();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Proxmox.Async.api2({
|
|
||||||
url: `${view.url}/${userid}/recovery`,
|
|
||||||
method: 'GET',
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
} catch (_response) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
init: function(view) {
|
|
||||||
this.onUseridChange(null, Proxmox.UserName);
|
|
||||||
},
|
|
||||||
|
|
||||||
onUseridChange: async function(field, userid) {
|
|
||||||
let me = this;
|
|
||||||
let vm = me.getViewModel();
|
|
||||||
|
|
||||||
me.userid = userid;
|
|
||||||
vm.set('userid', userid);
|
|
||||||
|
|
||||||
let has_entry = await me.hasEntry(userid);
|
|
||||||
vm.set('has_entry', has_entry);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
xtype: 'pmxDisplayEditField',
|
|
||||||
name: 'userid',
|
|
||||||
cbind: {
|
|
||||||
editable: (get) => !get('fixedUser'),
|
|
||||||
value: () => Proxmox.UserName,
|
|
||||||
},
|
|
||||||
fieldLabel: gettext('User'),
|
|
||||||
editConfig: {
|
|
||||||
xtype: 'pbsUserSelector',
|
|
||||||
allowBlank: false,
|
|
||||||
validator: function(_value) {
|
|
||||||
return !this.up('window').getViewModel().get('has_entry');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
renderer: Ext.String.htmlEncode,
|
|
||||||
listeners: {
|
|
||||||
change: 'onUseridChange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'hiddenfield',
|
|
||||||
name: 'type',
|
|
||||||
value: 'recovery',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'displayfield',
|
|
||||||
bind: {
|
|
||||||
hidden: '{!has_entry}',
|
|
||||||
},
|
|
||||||
hidden: true,
|
|
||||||
userCls: 'pmx-hint',
|
|
||||||
value: gettext('User already has recovery keys.'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'textfield',
|
|
||||||
name: 'password',
|
|
||||||
reference: 'password',
|
|
||||||
fieldLabel: gettext('Verify Password'),
|
|
||||||
inputType: 'password',
|
|
||||||
minLength: 5,
|
|
||||||
allowBlank: false,
|
|
||||||
validateBlank: true,
|
|
||||||
cbind: {
|
|
||||||
hidden: () => Proxmox.UserName === 'root@pam',
|
|
||||||
disabled: () => Proxmox.UserName === 'root@pam',
|
|
||||||
emptyText: () =>
|
|
||||||
Ext.String.format(gettext("Confirm your ({0}) password"), Proxmox.UserName),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
Ext.define('PBS.window.TfaRecoveryShow', {
|
|
||||||
extend: 'Ext.window.Window',
|
|
||||||
alias: ['widget.pbsTfaRecoveryShow'],
|
|
||||||
mixins: ['Proxmox.Mixin.CBind'],
|
|
||||||
|
|
||||||
width: 600,
|
|
||||||
modal: true,
|
|
||||||
resizable: false,
|
|
||||||
title: gettext('Recovery Keys'),
|
|
||||||
onEsc: Ext.emptyFn,
|
|
||||||
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
xtype: 'form',
|
|
||||||
layout: 'anchor',
|
|
||||||
bodyPadding: 10,
|
|
||||||
border: false,
|
|
||||||
fieldDefaults: {
|
|
||||||
anchor: '100%',
|
|
||||||
},
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
xtype: 'textarea',
|
|
||||||
editable: false,
|
|
||||||
inputId: 'token-secret-value',
|
|
||||||
cbind: {
|
|
||||||
value: '{values}',
|
|
||||||
},
|
|
||||||
fieldStyle: {
|
|
||||||
'fontFamily': 'monospace',
|
|
||||||
},
|
|
||||||
height: '160px',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'displayfield',
|
|
||||||
border: false,
|
|
||||||
padding: '5 0 0 0',
|
|
||||||
userCls: 'pmx-hint',
|
|
||||||
value: gettext('Please record recovery keys - they will only be displayed now'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
handler: function(b) {
|
|
||||||
document.getElementById('token-secret-value').select();
|
|
||||||
document.execCommand("copy");
|
|
||||||
},
|
|
||||||
iconCls: 'fa fa-clipboard',
|
|
||||||
text: gettext('Copy Recovery Keys'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
handler: function(b) {
|
|
||||||
let win = this.up('window');
|
|
||||||
win.paperkeys(win.values, win.userid);
|
|
||||||
},
|
|
||||||
iconCls: 'fa fa-print',
|
|
||||||
text: gettext('Print Recovery Keys'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
paperkeys: function(keyString, userid) {
|
|
||||||
let me = this;
|
|
||||||
|
|
||||||
let printFrame = document.createElement("iframe");
|
|
||||||
Object.assign(printFrame.style, {
|
|
||||||
position: "fixed",
|
|
||||||
right: "0",
|
|
||||||
bottom: "0",
|
|
||||||
width: "0",
|
|
||||||
height: "0",
|
|
||||||
border: "0",
|
|
||||||
});
|
|
||||||
const host = document.location.host;
|
|
||||||
const title = document.title;
|
|
||||||
const html = `<html><head><script>
|
|
||||||
window.addEventListener('DOMContentLoaded', (ev) => window.print());
|
|
||||||
</script><style>@media print and (max-height: 150mm) {
|
|
||||||
h4, p { margin: 0; font-size: 1em; }
|
|
||||||
}</style></head><body style="padding: 5px;">
|
|
||||||
<h4>Recovery Keys for '${userid}' - ${title} (${host})</h4>
|
|
||||||
<p style="font-size:1.5em;line-height:1.5em;font-family:monospace;
|
|
||||||
white-space:pre-wrap;overflow-wrap:break-word;">
|
|
||||||
${keyString}
|
|
||||||
</p>
|
|
||||||
</body></html>`;
|
|
||||||
|
|
||||||
printFrame.src = "data:text/html;base64," + btoa(html);
|
|
||||||
document.body.appendChild(printFrame);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,294 +0,0 @@
|
||||||
/*global QRCode*/
|
|
||||||
Ext.define('PBS.window.AddTotp', {
|
|
||||||
extend: 'Proxmox.window.Edit',
|
|
||||||
alias: 'widget.pbsAddTotp',
|
|
||||||
mixins: ['Proxmox.Mixin.CBind'],
|
|
||||||
|
|
||||||
onlineHelp: 'user_mgmt',
|
|
||||||
|
|
||||||
modal: true,
|
|
||||||
resizable: false,
|
|
||||||
title: gettext('Add a TOTP login factor'),
|
|
||||||
width: 512,
|
|
||||||
layout: {
|
|
||||||
type: 'vbox',
|
|
||||||
align: 'stretch',
|
|
||||||
},
|
|
||||||
|
|
||||||
isAdd: true,
|
|
||||||
userid: undefined,
|
|
||||||
tfa_id: undefined,
|
|
||||||
fixedUser: false,
|
|
||||||
|
|
||||||
updateQrCode: function() {
|
|
||||||
let me = this;
|
|
||||||
let values = me.lookup('totp_form').getValues();
|
|
||||||
let algorithm = values.algorithm;
|
|
||||||
if (!algorithm) {
|
|
||||||
algorithm = 'SHA1';
|
|
||||||
}
|
|
||||||
|
|
||||||
let otpuri =
|
|
||||||
'otpauth://totp/' +
|
|
||||||
encodeURIComponent(values.issuer) +
|
|
||||||
':' +
|
|
||||||
encodeURIComponent(values.userid) +
|
|
||||||
'?secret=' + values.secret +
|
|
||||||
'&period=' + values.step +
|
|
||||||
'&digits=' + values.digits +
|
|
||||||
'&algorithm=' + algorithm +
|
|
||||||
'&issuer=' + encodeURIComponent(values.issuer);
|
|
||||||
|
|
||||||
me.getController().getViewModel().set('otpuri', otpuri);
|
|
||||||
me.qrcode.makeCode(otpuri);
|
|
||||||
me.lookup('challenge').setVisible(true);
|
|
||||||
me.down('#qrbox').setVisible(true);
|
|
||||||
},
|
|
||||||
|
|
||||||
viewModel: {
|
|
||||||
data: {
|
|
||||||
valid: false,
|
|
||||||
secret: '',
|
|
||||||
otpuri: '',
|
|
||||||
userid: null,
|
|
||||||
},
|
|
||||||
|
|
||||||
formulas: {
|
|
||||||
secretEmpty: function(get) {
|
|
||||||
return get('secret').length === 0;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
controller: {
|
|
||||||
xclass: 'Ext.app.ViewController',
|
|
||||||
control: {
|
|
||||||
'field[qrupdate=true]': {
|
|
||||||
change: function() {
|
|
||||||
this.getView().updateQrCode();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'field': {
|
|
||||||
validitychange: function(field, valid) {
|
|
||||||
let me = this;
|
|
||||||
let viewModel = me.getViewModel();
|
|
||||||
let form = me.lookup('totp_form');
|
|
||||||
let challenge = me.lookup('challenge');
|
|
||||||
let password = me.lookup('password');
|
|
||||||
viewModel.set('valid', form.isValid() && challenge.isValid() && password.isValid());
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'#': {
|
|
||||||
show: function() {
|
|
||||||
let me = this;
|
|
||||||
let view = me.getView();
|
|
||||||
|
|
||||||
view.qrdiv = document.createElement('div');
|
|
||||||
view.qrcode = new QRCode(view.qrdiv, {
|
|
||||||
width: 256,
|
|
||||||
height: 256,
|
|
||||||
correctLevel: QRCode.CorrectLevel.M,
|
|
||||||
});
|
|
||||||
view.down('#qrbox').getEl().appendChild(view.qrdiv);
|
|
||||||
|
|
||||||
view.getController().randomizeSecret();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
randomizeSecret: function() {
|
|
||||||
let me = this;
|
|
||||||
let rnd = new Uint8Array(32);
|
|
||||||
window.crypto.getRandomValues(rnd);
|
|
||||||
let data = '';
|
|
||||||
rnd.forEach(function(b) {
|
|
||||||
// secret must be base32, so just use the first 5 bits
|
|
||||||
b = b & 0x1f;
|
|
||||||
if (b < 26) {
|
|
||||||
// A..Z
|
|
||||||
data += String.fromCharCode(b + 0x41);
|
|
||||||
} else {
|
|
||||||
// 2..7
|
|
||||||
data += String.fromCharCode(b-26 + 0x32);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
me.getViewModel().set('secret', data);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
xtype: 'form',
|
|
||||||
layout: 'anchor',
|
|
||||||
border: false,
|
|
||||||
reference: 'totp_form',
|
|
||||||
fieldDefaults: {
|
|
||||||
anchor: '100%',
|
|
||||||
},
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
xtype: 'pmxDisplayEditField',
|
|
||||||
name: 'userid',
|
|
||||||
cbind: {
|
|
||||||
editable: (get) => get('isAdd') && !get('fixedUser'),
|
|
||||||
value: () => Proxmox.UserName,
|
|
||||||
},
|
|
||||||
fieldLabel: gettext('User'),
|
|
||||||
editConfig: {
|
|
||||||
xtype: 'pbsUserSelector',
|
|
||||||
allowBlank: false,
|
|
||||||
},
|
|
||||||
renderer: Ext.String.htmlEncode,
|
|
||||||
listeners: {
|
|
||||||
change: function(field, newValue, oldValue) {
|
|
||||||
let vm = this.up('window').getViewModel();
|
|
||||||
vm.set('userid', newValue);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
qrupdate: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'textfield',
|
|
||||||
fieldLabel: gettext('Description'),
|
|
||||||
emptyText: gettext('For example: TFA device ID, required to identify multiple factors.'),
|
|
||||||
allowBlank: false,
|
|
||||||
name: 'description',
|
|
||||||
maxLength: 256,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
layout: 'hbox',
|
|
||||||
border: false,
|
|
||||||
padding: '0 0 5 0',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
xtype: 'textfield',
|
|
||||||
fieldLabel: gettext('Secret'),
|
|
||||||
emptyText: gettext('Unchanged'),
|
|
||||||
name: 'secret',
|
|
||||||
reference: 'tfa_secret',
|
|
||||||
regex: /^[A-Z2-7=]+$/,
|
|
||||||
regexText: 'Must be base32 [A-Z2-7=]',
|
|
||||||
maskRe: /[A-Z2-7=]/,
|
|
||||||
qrupdate: true,
|
|
||||||
bind: {
|
|
||||||
value: "{secret}",
|
|
||||||
},
|
|
||||||
flex: 4,
|
|
||||||
padding: '0 5 0 0',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'button',
|
|
||||||
text: gettext('Randomize'),
|
|
||||||
reference: 'randomize_button',
|
|
||||||
handler: 'randomizeSecret',
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'numberfield',
|
|
||||||
fieldLabel: gettext('Time period'),
|
|
||||||
name: 'step',
|
|
||||||
// Google Authenticator ignores this and generates bogus data
|
|
||||||
hidden: true,
|
|
||||||
value: 30,
|
|
||||||
minValue: 10,
|
|
||||||
qrupdate: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'numberfield',
|
|
||||||
fieldLabel: gettext('Digits'),
|
|
||||||
name: 'digits',
|
|
||||||
value: 6,
|
|
||||||
// Google Authenticator ignores this and generates bogus data
|
|
||||||
hidden: true,
|
|
||||||
minValue: 6,
|
|
||||||
maxValue: 8,
|
|
||||||
qrupdate: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'textfield',
|
|
||||||
fieldLabel: gettext('Issuer Name'),
|
|
||||||
name: 'issuer',
|
|
||||||
value: `Proxmox Backup Server - ${Proxmox.NodeName}`,
|
|
||||||
qrupdate: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'box',
|
|
||||||
itemId: 'qrbox',
|
|
||||||
visible: false, // will be enabled when generating a qr code
|
|
||||||
bind: {
|
|
||||||
visible: '{!secretEmpty}',
|
|
||||||
},
|
|
||||||
style: {
|
|
||||||
'background-color': 'white',
|
|
||||||
'margin-left': 'auto',
|
|
||||||
'margin-right': 'auto',
|
|
||||||
padding: '5px',
|
|
||||||
width: '266px',
|
|
||||||
height: '266px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'textfield',
|
|
||||||
fieldLabel: gettext('Verify Code'),
|
|
||||||
allowBlank: false,
|
|
||||||
reference: 'challenge',
|
|
||||||
name: 'challenge',
|
|
||||||
bind: {
|
|
||||||
disabled: '{!showTOTPVerifiction}',
|
|
||||||
visible: '{showTOTPVerifiction}',
|
|
||||||
},
|
|
||||||
emptyText: gettext('Scan QR code in a TOTP app and enter an auth. code here'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'textfield',
|
|
||||||
name: 'password',
|
|
||||||
reference: 'password',
|
|
||||||
fieldLabel: gettext('Verify Password'),
|
|
||||||
inputType: 'password',
|
|
||||||
minLength: 5,
|
|
||||||
allowBlank: false,
|
|
||||||
validateBlank: true,
|
|
||||||
cbind: {
|
|
||||||
hidden: () => Proxmox.UserName === 'root@pam',
|
|
||||||
disabled: () => Proxmox.UserName === 'root@pam',
|
|
||||||
emptyText: () =>
|
|
||||||
Ext.String.format(gettext("Confirm your ({0}) password"), Proxmox.UserName),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
initComponent: function() {
|
|
||||||
let me = this;
|
|
||||||
me.url = '/api2/extjs/access/tfa/';
|
|
||||||
me.method = 'POST';
|
|
||||||
me.callParent();
|
|
||||||
},
|
|
||||||
|
|
||||||
getValues: function(dirtyOnly) {
|
|
||||||
let me = this;
|
|
||||||
let viewmodel = me.getController().getViewModel();
|
|
||||||
|
|
||||||
let values = me.callParent(arguments);
|
|
||||||
|
|
||||||
let uid = encodeURIComponent(values.userid);
|
|
||||||
me.url = `/api2/extjs/access/tfa/${uid}`;
|
|
||||||
delete values.userid;
|
|
||||||
|
|
||||||
let data = {
|
|
||||||
description: values.description,
|
|
||||||
type: "totp",
|
|
||||||
totp: viewmodel.get('otpuri'),
|
|
||||||
value: values.challenge,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (values.password) {
|
|
||||||
data.password = values.password;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,226 +0,0 @@
|
||||||
Ext.define('PBS.window.AddWebauthn', {
|
|
||||||
extend: 'Ext.window.Window',
|
|
||||||
alias: 'widget.pbsAddWebauthn',
|
|
||||||
mixins: ['Proxmox.Mixin.CBind'],
|
|
||||||
|
|
||||||
onlineHelp: 'user_mgmt',
|
|
||||||
|
|
||||||
modal: true,
|
|
||||||
resizable: false,
|
|
||||||
title: gettext('Add a Webauthn login token'),
|
|
||||||
width: 512,
|
|
||||||
|
|
||||||
user: undefined,
|
|
||||||
fixedUser: false,
|
|
||||||
|
|
||||||
initComponent: function() {
|
|
||||||
let me = this;
|
|
||||||
me.callParent();
|
|
||||||
Ext.GlobalEvents.fireEvent('proxmoxShowHelp', me.onlineHelp);
|
|
||||||
},
|
|
||||||
|
|
||||||
viewModel: {
|
|
||||||
data: {
|
|
||||||
valid: false,
|
|
||||||
userid: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
controller: {
|
|
||||||
xclass: 'Ext.app.ViewController',
|
|
||||||
|
|
||||||
control: {
|
|
||||||
'field': {
|
|
||||||
validitychange: function(field, valid) {
|
|
||||||
let me = this;
|
|
||||||
let viewmodel = me.getViewModel();
|
|
||||||
let form = me.lookup('webauthn_form');
|
|
||||||
viewmodel.set('valid', form.isValid());
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'#': {
|
|
||||||
show: function() {
|
|
||||||
let me = this;
|
|
||||||
let view = me.getView();
|
|
||||||
|
|
||||||
if (Proxmox.UserName === 'root@pam') {
|
|
||||||
view.lookup('password').setVisible(false);
|
|
||||||
view.lookup('password').setDisabled(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
registerWebauthn: async function() {
|
|
||||||
let me = this;
|
|
||||||
let values = me.lookup('webauthn_form').getValues();
|
|
||||||
values.type = "webauthn";
|
|
||||||
|
|
||||||
let userid = values.user;
|
|
||||||
delete values.user;
|
|
||||||
|
|
||||||
me.getView().mask(gettext('Please wait...'), 'x-mask-loading');
|
|
||||||
|
|
||||||
try {
|
|
||||||
let register_response = await Proxmox.Async.api2({
|
|
||||||
url: `/api2/extjs/access/tfa/${userid}`,
|
|
||||||
method: 'POST',
|
|
||||||
params: values,
|
|
||||||
});
|
|
||||||
|
|
||||||
let data = register_response.result.data;
|
|
||||||
if (!data.challenge) {
|
|
||||||
throw "server did not respond with a challenge";
|
|
||||||
}
|
|
||||||
|
|
||||||
let creds = JSON.parse(data.challenge);
|
|
||||||
|
|
||||||
// Fix this up before passing it to the browser, but keep a copy of the original
|
|
||||||
// string to pass in the response:
|
|
||||||
let challenge_str = creds.publicKey.challenge;
|
|
||||||
creds.publicKey.challenge = PBS.Utils.base64url_to_bytes(challenge_str);
|
|
||||||
creds.publicKey.user.id =
|
|
||||||
PBS.Utils.base64url_to_bytes(creds.publicKey.user.id);
|
|
||||||
|
|
||||||
// convert existing authenticators structure
|
|
||||||
creds.publicKey.excludeCredentials =
|
|
||||||
(creds.publicKey.excludeCredentials || [])
|
|
||||||
.map((credential) => ({
|
|
||||||
id: PBS.Utils.base64url_to_bytes(credential.id),
|
|
||||||
type: credential.type,
|
|
||||||
}));
|
|
||||||
|
|
||||||
let msg = Ext.Msg.show({
|
|
||||||
title: `Webauthn: ${gettext('Setup')}`,
|
|
||||||
message: gettext('Please press the button on your Webauthn Device'),
|
|
||||||
buttons: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
let token_response;
|
|
||||||
try {
|
|
||||||
token_response = await navigator.credentials.create(creds);
|
|
||||||
} catch (error) {
|
|
||||||
let errmsg = error.message;
|
|
||||||
if (error.name === 'InvalidStateError') {
|
|
||||||
errmsg = gettext('Is this token already registered?');
|
|
||||||
}
|
|
||||||
throw gettext('An error occurred during token registration.') +
|
|
||||||
`<br>${error.name}: ${errmsg}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We cannot pass ArrayBuffers to the API, so extract & convert the data.
|
|
||||||
let response = {
|
|
||||||
id: token_response.id,
|
|
||||||
type: token_response.type,
|
|
||||||
rawId: PBS.Utils.bytes_to_base64url(token_response.rawId),
|
|
||||||
response: {
|
|
||||||
attestationObject: PBS.Utils.bytes_to_base64url(
|
|
||||||
token_response.response.attestationObject,
|
|
||||||
),
|
|
||||||
clientDataJSON: PBS.Utils.bytes_to_base64url(
|
|
||||||
token_response.response.clientDataJSON,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
msg.close();
|
|
||||||
|
|
||||||
let params = {
|
|
||||||
type: "webauthn",
|
|
||||||
challenge: challenge_str,
|
|
||||||
value: JSON.stringify(response),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (values.password) {
|
|
||||||
params.password = values.password;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Proxmox.Async.api2({
|
|
||||||
url: `/api2/extjs/access/tfa/${userid}`,
|
|
||||||
method: 'POST',
|
|
||||||
params,
|
|
||||||
});
|
|
||||||
} catch (response) {
|
|
||||||
let error = response.result.message;
|
|
||||||
console.error(error); // for debugging if it's not displayable...
|
|
||||||
Ext.Msg.alert(gettext('Error'), error);
|
|
||||||
}
|
|
||||||
|
|
||||||
me.getView().close();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
xtype: 'form',
|
|
||||||
reference: 'webauthn_form',
|
|
||||||
layout: 'anchor',
|
|
||||||
border: false,
|
|
||||||
bodyPadding: 10,
|
|
||||||
fieldDefaults: {
|
|
||||||
anchor: '100%',
|
|
||||||
},
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
xtype: 'pmxDisplayEditField',
|
|
||||||
name: 'user',
|
|
||||||
cbind: {
|
|
||||||
editable: (get) => !get('fixedUser'),
|
|
||||||
value: () => Proxmox.UserName,
|
|
||||||
},
|
|
||||||
fieldLabel: gettext('User'),
|
|
||||||
editConfig: {
|
|
||||||
xtype: 'pbsUserSelector',
|
|
||||||
allowBlank: false,
|
|
||||||
},
|
|
||||||
renderer: Ext.String.htmlEncode,
|
|
||||||
listeners: {
|
|
||||||
change: function(field, newValue, oldValue) {
|
|
||||||
let vm = this.up('window').getViewModel();
|
|
||||||
vm.set('userid', newValue);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'textfield',
|
|
||||||
fieldLabel: gettext('Description'),
|
|
||||||
allowBlank: false,
|
|
||||||
name: 'description',
|
|
||||||
maxLength: 256,
|
|
||||||
emptyText: gettext('For example: TFA device ID, required to identify multiple factors.'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'textfield',
|
|
||||||
name: 'password',
|
|
||||||
reference: 'password',
|
|
||||||
fieldLabel: gettext('Verify Password'),
|
|
||||||
inputType: 'password',
|
|
||||||
minLength: 5,
|
|
||||||
allowBlank: false,
|
|
||||||
validateBlank: true,
|
|
||||||
cbind: {
|
|
||||||
hidden: () => Proxmox.UserName === 'root@pam',
|
|
||||||
disabled: () => Proxmox.UserName === 'root@pam',
|
|
||||||
emptyText: () =>
|
|
||||||
Ext.String.format(gettext("Confirm your ({0}) password"), Proxmox.UserName),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
xtype: 'proxmoxHelpButton',
|
|
||||||
},
|
|
||||||
'->',
|
|
||||||
{
|
|
||||||
xtype: 'button',
|
|
||||||
text: gettext('Register Webauthn Device'),
|
|
||||||
handler: 'registerWebauthn',
|
|
||||||
bind: {
|
|
||||||
disabled: '{!valid}',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
|
@ -49,7 +49,7 @@ Ext.define('PBS.window.NotifyOptions', {
|
||||||
},
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
xtype: 'pbsUserSelector',
|
xtype: 'pmxUserSelector',
|
||||||
name: 'notify-user',
|
name: 'notify-user',
|
||||||
fieldLabel: gettext('Notify User'),
|
fieldLabel: gettext('Notify User'),
|
||||||
emptyText: 'root@pam',
|
emptyText: 'root@pam',
|
||||||
|
|
|
@ -1,93 +0,0 @@
|
||||||
Ext.define('PBS.window.TfaEdit', {
|
|
||||||
extend: 'Proxmox.window.Edit',
|
|
||||||
alias: 'widget.pbsTfaEdit',
|
|
||||||
mixins: ['Proxmox.Mixin.CBind'],
|
|
||||||
|
|
||||||
onlineHelp: 'user_mgmt',
|
|
||||||
|
|
||||||
modal: true,
|
|
||||||
resizable: false,
|
|
||||||
title: gettext("Modify a TFA entry's description"),
|
|
||||||
width: 512,
|
|
||||||
|
|
||||||
layout: {
|
|
||||||
type: 'vbox',
|
|
||||||
align: 'stretch',
|
|
||||||
},
|
|
||||||
|
|
||||||
cbindData: function(initialConfig) {
|
|
||||||
let me = this;
|
|
||||||
|
|
||||||
let tfa_id = initialConfig['tfa-id'];
|
|
||||||
me.tfa_id = tfa_id;
|
|
||||||
me.defaultFocus = 'textfield[name=description]';
|
|
||||||
me.url = `/api2/extjs/access/tfa/${tfa_id}`;
|
|
||||||
me.method = 'PUT';
|
|
||||||
me.autoLoad = true;
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
|
|
||||||
initComponent: function() {
|
|
||||||
let me = this;
|
|
||||||
me.callParent();
|
|
||||||
|
|
||||||
if (Proxmox.UserName === 'root@pam') {
|
|
||||||
me.lookup('password').setVisible(false);
|
|
||||||
me.lookup('password').setDisabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
let userid = me.tfa_id.split('/')[0];
|
|
||||||
me.lookup('userid').setValue(userid);
|
|
||||||
},
|
|
||||||
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
xtype: 'displayfield',
|
|
||||||
reference: 'userid',
|
|
||||||
editable: false,
|
|
||||||
fieldLabel: gettext('User'),
|
|
||||||
editConfig: {
|
|
||||||
xtype: 'pbsUserSelector',
|
|
||||||
allowBlank: false,
|
|
||||||
},
|
|
||||||
cbind: {
|
|
||||||
value: () => Proxmox.UserName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'proxmoxtextfield',
|
|
||||||
name: 'description',
|
|
||||||
allowBlank: false,
|
|
||||||
fieldLabel: gettext('Description'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'proxmoxcheckbox',
|
|
||||||
fieldLabel: gettext('Enabled'),
|
|
||||||
name: 'enable',
|
|
||||||
uncheckedValue: 0,
|
|
||||||
defaultValue: 1,
|
|
||||||
checked: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
xtype: 'textfield',
|
|
||||||
inputType: 'password',
|
|
||||||
fieldLabel: gettext('Password'),
|
|
||||||
minLength: 5,
|
|
||||||
reference: 'password',
|
|
||||||
name: 'password',
|
|
||||||
allowBlank: false,
|
|
||||||
validateBlank: true,
|
|
||||||
emptyText: gettext('verify current password'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
getValues: function() {
|
|
||||||
var me = this;
|
|
||||||
|
|
||||||
var values = me.callParent(arguments);
|
|
||||||
|
|
||||||
delete values.userid;
|
|
||||||
|
|
||||||
return values;
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -26,7 +26,7 @@ Ext.define('PBS.window.TokenEdit', {
|
||||||
value: () => Proxmox.UserName,
|
value: () => Proxmox.UserName,
|
||||||
},
|
},
|
||||||
editConfig: {
|
editConfig: {
|
||||||
xtype: 'pbsUserSelector',
|
xtype: 'pmxUserSelector',
|
||||||
allowBlank: false,
|
allowBlank: false,
|
||||||
},
|
},
|
||||||
name: 'user',
|
name: 'user',
|
||||||
|
|
Loading…
Reference in New Issue