2019-01-30 14:14:20 +00:00
|
|
|
Ext.define('PBS.LoginView', {
|
|
|
|
extend: 'Ext.container.Container',
|
|
|
|
xtype: 'loginview',
|
|
|
|
|
|
|
|
controller: {
|
|
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
|
2020-11-02 13:36:10 +00:00
|
|
|
submitForm: async function() {
|
2019-01-30 14:14:20 +00:00
|
|
|
var me = this;
|
|
|
|
var loginForm = me.lookupReference('loginForm');
|
2020-04-09 11:37:14 +00:00
|
|
|
var unField = me.lookupReference('usernameField');
|
|
|
|
var saveunField = me.lookupReference('saveunField');
|
2019-01-30 14:14:20 +00:00
|
|
|
|
2020-04-09 11:37:14 +00:00
|
|
|
if (!loginForm.isValid()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let params = loginForm.getValues();
|
|
|
|
|
|
|
|
params.username = params.username + '@' + params.realm;
|
2020-09-25 16:34:54 +00:00
|
|
|
delete params.realm;
|
2020-04-09 11:37:14 +00:00
|
|
|
|
|
|
|
if (loginForm.isVisible()) {
|
|
|
|
loginForm.mask(gettext('Please wait...'), 'x-mask-loading');
|
2019-01-30 14:14:20 +00:00
|
|
|
}
|
2020-04-09 11:37:14 +00:00
|
|
|
|
|
|
|
// set or clear username
|
|
|
|
var sp = Ext.state.Manager.getProvider();
|
|
|
|
if (saveunField.getValue() === true) {
|
|
|
|
sp.set(unField.getStateId(), unField.getValue());
|
|
|
|
} else {
|
|
|
|
sp.clear(unField.getStateId());
|
|
|
|
}
|
|
|
|
sp.set(saveunField.getStateId(), saveunField.getValue());
|
|
|
|
|
2020-11-02 13:36:10 +00:00
|
|
|
try {
|
|
|
|
let resp = await PBS.Async.api2({
|
|
|
|
url: '/api2/extjs/access/ticket',
|
|
|
|
params: params,
|
|
|
|
method: 'POST',
|
|
|
|
});
|
|
|
|
|
|
|
|
let data = resp.result.data;
|
|
|
|
if (data.ticket.startsWith("PBS:!tfa!")) {
|
|
|
|
data = await me.performTFAChallenge(data);
|
|
|
|
}
|
|
|
|
|
|
|
|
PBS.Utils.updateLoginData(data);
|
|
|
|
PBS.app.changeView('mainview');
|
|
|
|
} catch (error) {
|
|
|
|
Proxmox.Utils.authClear();
|
|
|
|
loginForm.unmask();
|
|
|
|
Ext.MessageBox.alert(
|
|
|
|
gettext('Error'),
|
|
|
|
gettext('Login failed. Please try again'),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
performTFAChallenge: async function(data) {
|
|
|
|
let me = this;
|
|
|
|
|
|
|
|
let userid = data.username;
|
|
|
|
let ticket = data.ticket;
|
|
|
|
let challenge = JSON.parse(decodeURIComponent(
|
|
|
|
ticket.split(':')[1].slice("!tfa!".length),
|
|
|
|
));
|
|
|
|
|
|
|
|
let resp = await new Promise((resolve, reject) => {
|
|
|
|
Ext.create('PBS.login.TfaWindow', {
|
|
|
|
userid,
|
|
|
|
ticket,
|
|
|
|
challenge,
|
|
|
|
onResolve: value => resolve(value),
|
|
|
|
onReject: reject,
|
|
|
|
}).show();
|
2020-04-09 11:37:14 +00:00
|
|
|
});
|
2020-11-02 13:36:10 +00:00
|
|
|
|
|
|
|
return resp.result.data;
|
2019-01-30 14:14:20 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
control: {
|
2020-04-09 11:37:14 +00:00
|
|
|
'field[name=username]': {
|
|
|
|
specialkey: function(f, e) {
|
|
|
|
if (e.getKey() === e.ENTER) {
|
|
|
|
var pf = this.lookupReference('passwordField');
|
|
|
|
if (!pf.getValue()) {
|
|
|
|
pf.focus(false);
|
|
|
|
}
|
|
|
|
}
|
2020-09-25 16:34:54 +00:00
|
|
|
},
|
2020-04-09 11:37:14 +00:00
|
|
|
},
|
|
|
|
'field[name=lang]': {
|
|
|
|
change: function(f, value) {
|
|
|
|
var dt = Ext.Date.add(new Date(), Ext.Date.YEAR, 10);
|
|
|
|
Ext.util.Cookies.set('PBSLangCookie', value, dt);
|
|
|
|
this.getView().mask(gettext('Please wait...'), 'x-mask-loading');
|
|
|
|
window.location.reload();
|
2020-09-25 16:34:54 +00:00
|
|
|
},
|
2020-04-09 11:37:14 +00:00
|
|
|
},
|
2019-01-30 14:14:20 +00:00
|
|
|
'button[reference=loginButton]': {
|
2020-09-25 16:34:54 +00:00
|
|
|
click: 'submitForm',
|
2020-04-09 11:37:14 +00:00
|
|
|
},
|
|
|
|
'window[reference=loginwindow]': {
|
|
|
|
show: function() {
|
|
|
|
var sp = Ext.state.Manager.getProvider();
|
|
|
|
var checkboxField = this.lookupReference('saveunField');
|
|
|
|
var unField = this.lookupReference('usernameField');
|
|
|
|
|
|
|
|
var checked = sp.get(checkboxField.getStateId());
|
|
|
|
checkboxField.setValue(checked);
|
|
|
|
|
2020-09-25 16:34:54 +00:00
|
|
|
if (checked === true) {
|
2020-04-09 11:37:14 +00:00
|
|
|
var username = sp.get(unField.getStateId());
|
|
|
|
unField.setValue(username);
|
|
|
|
var pwField = this.lookupReference('passwordField');
|
|
|
|
pwField.focus();
|
|
|
|
}
|
2020-09-25 16:34:54 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2019-01-30 14:14:20 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
plugins: 'viewport',
|
|
|
|
|
|
|
|
layout: {
|
2020-09-25 16:34:54 +00:00
|
|
|
type: 'border',
|
2019-01-30 14:14:20 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
items: [
|
|
|
|
{
|
|
|
|
region: 'north',
|
|
|
|
xtype: 'container',
|
|
|
|
layout: {
|
|
|
|
type: 'hbox',
|
2020-09-25 16:34:54 +00:00
|
|
|
align: 'middle',
|
2019-01-30 14:14:20 +00:00
|
|
|
},
|
|
|
|
margin: '2 5 2 5',
|
|
|
|
height: 38,
|
|
|
|
items: [
|
|
|
|
{
|
2020-05-18 12:18:37 +00:00
|
|
|
xtype: 'proxmoxlogo',
|
|
|
|
prefix: '',
|
2019-01-30 14:14:20 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
xtype: 'versioninfo',
|
2020-09-25 16:34:54 +00:00
|
|
|
makeApiCall: false,
|
|
|
|
},
|
|
|
|
],
|
2019-01-30 14:14:20 +00:00
|
|
|
},
|
|
|
|
{
|
2020-09-25 16:34:54 +00:00
|
|
|
region: 'center',
|
2019-01-30 14:14:20 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
xtype: 'window',
|
|
|
|
closable: false,
|
|
|
|
resizable: false,
|
|
|
|
reference: 'loginwindow',
|
|
|
|
autoShow: true,
|
|
|
|
modal: true,
|
2020-04-09 11:37:14 +00:00
|
|
|
width: 400,
|
2019-01-30 14:14:20 +00:00
|
|
|
|
2020-04-09 11:37:14 +00:00
|
|
|
defaultFocus: 'usernameField',
|
2019-01-30 14:14:20 +00:00
|
|
|
|
|
|
|
layout: {
|
2020-09-25 16:34:54 +00:00
|
|
|
type: 'auto',
|
2019-01-30 14:14:20 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
title: gettext('Proxmox Backup Server Login'),
|
|
|
|
|
|
|
|
items: [
|
|
|
|
{
|
|
|
|
xtype: 'form',
|
|
|
|
layout: {
|
2020-09-25 16:34:54 +00:00
|
|
|
type: 'form',
|
2019-01-30 14:14:20 +00:00
|
|
|
},
|
|
|
|
defaultButton: 'loginButton',
|
|
|
|
url: '/api2/extjs/access/ticket',
|
|
|
|
reference: 'loginForm',
|
|
|
|
|
|
|
|
fieldDefaults: {
|
|
|
|
labelAlign: 'right',
|
2020-09-25 16:34:54 +00:00
|
|
|
allowBlank: false,
|
2019-01-30 14:14:20 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
items: [
|
|
|
|
{
|
|
|
|
xtype: 'textfield',
|
|
|
|
fieldLabel: gettext('User name'),
|
|
|
|
name: 'username',
|
|
|
|
itemId: 'usernameField',
|
2020-04-09 11:37:14 +00:00
|
|
|
reference: 'usernameField',
|
2020-09-25 16:34:54 +00:00
|
|
|
stateId: 'login-username',
|
2019-01-30 14:14:20 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
xtype: 'textfield',
|
|
|
|
inputType: 'password',
|
|
|
|
fieldLabel: gettext('Password'),
|
|
|
|
name: 'password',
|
2019-12-17 10:35:13 +00:00
|
|
|
itemId: 'passwordField',
|
|
|
|
reference: 'passwordField',
|
2020-04-09 11:37:14 +00:00
|
|
|
},
|
2020-05-18 12:18:37 +00:00
|
|
|
{
|
|
|
|
xtype: 'pmxRealmComboBox',
|
2020-09-25 16:34:54 +00:00
|
|
|
name: 'realm',
|
2020-05-18 12:18:37 +00:00
|
|
|
},
|
2020-04-09 11:37:14 +00:00
|
|
|
{
|
|
|
|
xtype: 'proxmoxLanguageSelector',
|
|
|
|
fieldLabel: gettext('Language'),
|
|
|
|
value: Ext.util.Cookies.get('PBSLangCookie') || Proxmox.defaultLang || 'en',
|
|
|
|
name: 'lang',
|
|
|
|
reference: 'langField',
|
2020-09-25 16:34:54 +00:00
|
|
|
submitValue: false,
|
|
|
|
},
|
2019-01-30 14:14:20 +00:00
|
|
|
],
|
|
|
|
buttons: [
|
2020-04-09 11:37:14 +00:00
|
|
|
{
|
|
|
|
xtype: 'checkbox',
|
|
|
|
fieldLabel: gettext('Save User name'),
|
|
|
|
name: 'saveusername',
|
|
|
|
reference: 'saveunField',
|
|
|
|
stateId: 'login-saveusername',
|
|
|
|
labelWidth: 250,
|
|
|
|
labelAlign: 'right',
|
2020-09-25 16:34:54 +00:00
|
|
|
submitValue: false,
|
2020-04-09 11:37:14 +00:00
|
|
|
},
|
2019-01-30 14:14:20 +00:00
|
|
|
{
|
|
|
|
text: gettext('Login'),
|
|
|
|
reference: 'loginButton',
|
2020-09-25 16:34:54 +00:00
|
|
|
formBind: true,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
],
|
2019-01-30 14:14:20 +00:00
|
|
|
});
|
2020-11-02 13:36:10 +00:00
|
|
|
|
|
|
|
Ext.define('PBS.login.TfaWindow', {
|
|
|
|
extend: 'Ext.window.Window',
|
|
|
|
mixins: ['Proxmox.Mixin.CBind'],
|
|
|
|
|
|
|
|
title: gettext("Second login factor required"),
|
|
|
|
|
2021-01-27 12:19:09 +00:00
|
|
|
modal: true,
|
|
|
|
resizable: false,
|
2020-11-02 13:36:10 +00:00
|
|
|
width: 512,
|
|
|
|
layout: {
|
|
|
|
type: 'vbox',
|
|
|
|
align: 'stretch',
|
|
|
|
},
|
|
|
|
|
2021-01-27 12:19:09 +00:00
|
|
|
defaultButton: 'tfaButton',
|
|
|
|
|
|
|
|
viewModel: {
|
|
|
|
data: {
|
2021-02-01 10:48:33 +00:00
|
|
|
confirmText: gettext('Confirm Second Factor'),
|
2021-01-27 12:19:09 +00:00
|
|
|
canConfirm: false,
|
|
|
|
availabelChallenge: {},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
cancelled: true,
|
2020-11-02 13:36:10 +00:00
|
|
|
|
|
|
|
controller: {
|
|
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
|
|
|
|
init: function(view) {
|
|
|
|
let me = this;
|
2021-01-27 12:19:09 +00:00
|
|
|
let vm = me.getViewModel();
|
2020-11-02 13:36:10 +00:00
|
|
|
|
|
|
|
if (!view.userid) {
|
|
|
|
throw "no userid given";
|
|
|
|
}
|
|
|
|
if (!view.ticket) {
|
|
|
|
throw "no ticket given";
|
|
|
|
}
|
2021-01-27 12:19:09 +00:00
|
|
|
const challenge = view.challenge;
|
|
|
|
if (!challenge) {
|
2020-11-02 13:36:10 +00:00
|
|
|
throw "no challenge given";
|
|
|
|
}
|
|
|
|
|
2021-01-27 17:44:59 +00:00
|
|
|
let lastTabId = me.getLastTabUsed();
|
|
|
|
let initialTab = -1, i = 0;
|
2021-01-27 12:19:09 +00:00
|
|
|
for (const k of ['webauthn', 'totp', 'recovery']) {
|
|
|
|
const available = !!challenge[k];
|
|
|
|
vm.set(`availabelChallenge.${k}`, available);
|
2020-11-02 13:36:10 +00:00
|
|
|
|
2021-01-27 17:44:59 +00:00
|
|
|
if (available) {
|
|
|
|
if (i === lastTabId) {
|
|
|
|
initialTab = i;
|
|
|
|
} else if (initialTab < 0) {
|
|
|
|
initialTab = i;
|
|
|
|
}
|
2021-01-27 12:19:09 +00:00
|
|
|
}
|
|
|
|
i++;
|
2020-11-02 13:36:10 +00:00
|
|
|
}
|
2021-01-27 17:44:59 +00:00
|
|
|
view.down('tabpanel').setActiveTab(initialTab);
|
2020-11-02 13:36:10 +00:00
|
|
|
|
2021-01-27 12:19:09 +00:00
|
|
|
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);
|
|
|
|
}
|
2020-11-02 13:36:10 +00:00
|
|
|
}
|
|
|
|
|
2021-01-27 18:38:36 +00:00
|
|
|
if (challenge.webauthn && initialTab === 0) {
|
2020-11-02 13:36:10 +00:00
|
|
|
let _promise = me.loginWebauthn();
|
|
|
|
}
|
|
|
|
},
|
2021-01-27 12:19:09 +00:00
|
|
|
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();
|
|
|
|
}
|
2021-02-01 10:48:33 +00:00
|
|
|
|
|
|
|
let confirmText = newCard.confirmText || gettext('Confirm Second Factor');
|
|
|
|
this.getViewModel().set('confirmText', confirmText);
|
|
|
|
|
2021-01-27 17:44:59 +00:00
|
|
|
this.saveLastTabUsed(tabPanel, newCard);
|
2021-01-27 12:19:09 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
'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);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2020-11-02 13:36:10 +00:00
|
|
|
|
2021-01-27 17:44:59 +00:00
|
|
|
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;
|
|
|
|
},
|
|
|
|
|
2020-11-02 13:36:10 +00:00
|
|
|
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;
|
|
|
|
|
2021-01-27 12:19:09 +00:00
|
|
|
let code = me.lookup('totp').getValue();
|
|
|
|
let _promise = me.finishChallenge(`totp:${code}`);
|
2020-11-02 13:36:10 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
loginWebauthn: async function() {
|
|
|
|
let me = this;
|
|
|
|
let view = me.getView();
|
|
|
|
|
2021-01-27 12:19:09 +00:00
|
|
|
me.lookup('webAuthnWaiting').setVisible(true);
|
2020-11-02 13:36:10 +00:00
|
|
|
|
|
|
|
let challenge = view.challenge.webauthn;
|
|
|
|
|
2021-01-27 18:40:12 +00:00
|
|
|
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);
|
|
|
|
}
|
2020-11-02 13:36:10 +00:00
|
|
|
}
|
|
|
|
|
2021-01-27 12:19:09 +00:00
|
|
|
let controller = new AbortController();
|
|
|
|
challenge.signal = controller.signal;
|
|
|
|
|
2020-11-02 13:36:10 +00:00
|
|
|
let hwrsp;
|
|
|
|
try {
|
2021-01-27 12:19:09 +00:00
|
|
|
//Promise.race( ...
|
2020-11-02 13:36:10 +00:00
|
|
|
hwrsp = await navigator.credentials.get(challenge);
|
|
|
|
} catch (error) {
|
2021-01-27 18:40:12 +00:00
|
|
|
// 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, ...?
|
2020-11-02 13:36:10 +00:00
|
|
|
return;
|
|
|
|
} finally {
|
2021-01-27 12:19:09 +00:00
|
|
|
let waitingMessage = me.lookup('webAuthnWaiting');
|
|
|
|
if (waitingMessage) {
|
|
|
|
waitingMessage.setVisible(false);
|
|
|
|
}
|
2020-11-02 13:36:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
let response = {
|
|
|
|
id: hwrsp.id,
|
|
|
|
type: hwrsp.type,
|
2021-01-27 18:40:12 +00:00
|
|
|
challenge: challenge.string,
|
2020-11-02 13:36:10 +00:00
|
|
|
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;
|
|
|
|
|
2021-01-27 12:19:09 +00:00
|
|
|
let key = me.lookup('recoveryKey').getValue();
|
|
|
|
let _promise = me.finishChallenge(`recovery:${key}`);
|
|
|
|
},
|
|
|
|
|
|
|
|
loginTFA: function() {
|
|
|
|
let me = this;
|
2021-01-27 19:18:43 +00:00
|
|
|
// avoid triggering more than once during challenge
|
|
|
|
me.getViewModel().set('canConfirm', false);
|
2021-01-27 12:19:09 +00:00
|
|
|
let view = me.getView();
|
|
|
|
let tfaPanel = view.down('tabpanel').getActiveTab();
|
|
|
|
me[tfaPanel.handler]();
|
2020-11-02 13:36:10 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
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 PBS.Async.api2({
|
|
|
|
url: '/api2/extjs/access/ticket',
|
|
|
|
method: 'POST',
|
|
|
|
params,
|
|
|
|
})
|
|
|
|
.then(resolve)
|
|
|
|
.catch(reject);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
listeners: {
|
|
|
|
close: 'onClose',
|
|
|
|
},
|
|
|
|
|
2021-01-27 12:19:09 +00:00
|
|
|
items: [{
|
|
|
|
xtype: 'tabpanel',
|
|
|
|
region: 'center',
|
|
|
|
layout: 'fit',
|
|
|
|
bodyPadding: 10,
|
|
|
|
items: [
|
|
|
|
{
|
|
|
|
xtype: 'panel',
|
|
|
|
title: 'WebAuthn',
|
|
|
|
iconCls: 'fa fa-fw fa-shield',
|
2021-02-01 10:48:33 +00:00
|
|
|
confirmText: gettext('Start WebAuthn challenge'),
|
2021-01-27 12:19:09 +00:00
|
|
|
handler: 'loginWebauthn',
|
|
|
|
bind: {
|
|
|
|
disabled: '{!availabelChallenge.webauthn}',
|
2020-11-02 13:36:10 +00:00
|
|
|
},
|
2021-01-27 12:19:09 +00:00
|
|
|
items: [
|
|
|
|
{
|
|
|
|
xtype: 'box',
|
2021-01-27 18:44:07 +00:00
|
|
|
html: gettext('Please insert your authentication device and press its button'),
|
2021-01-27 12:19:09 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
xtype: 'box',
|
2021-01-27 18:44:07 +00:00
|
|
|
html: gettext('Waiting for second factor.') +`<i class="fa fa-refresh fa-spin fa-fw"></i>`,
|
2021-01-27 12:19:09 +00:00
|
|
|
reference: 'webAuthnWaiting',
|
|
|
|
hidden: true,
|
|
|
|
},
|
|
|
|
],
|
2020-11-02 13:36:10 +00:00
|
|
|
},
|
2021-01-27 12:19:09 +00:00
|
|
|
{
|
|
|
|
xtype: 'panel',
|
|
|
|
title: gettext('TOTP App'),
|
|
|
|
iconCls: 'fa fa-fw fa-clock-o',
|
|
|
|
handler: 'loginTotp',
|
|
|
|
bind: {
|
|
|
|
disabled: '{!availabelChallenge.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: 'TOTP codes consist of six decimal digits',
|
|
|
|
},
|
|
|
|
],
|
2021-01-18 11:46:47 +00:00
|
|
|
},
|
2021-01-27 12:19:09 +00:00
|
|
|
{
|
|
|
|
xtype: 'panel',
|
|
|
|
title: gettext('Recovery Key'),
|
|
|
|
iconCls: 'fa fa-fw fa-file-text-o',
|
|
|
|
handler: 'loginRecovery',
|
|
|
|
bind: {
|
|
|
|
disabled: '{!availabelChallenge.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: 'Does not looks like a valid recovery key',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
xtype: 'box',
|
|
|
|
reference: 'recoveryLow',
|
|
|
|
hidden: true,
|
|
|
|
html: '<i class="fa fa-exclamation-triangle warning"></i>'
|
2021-02-01 14:39:56 +00:00
|
|
|
+ gettext('Less than {0} recovery keys available. Please generate a new set after login!'),
|
2021-01-27 12:19:09 +00:00
|
|
|
},
|
|
|
|
],
|
2020-11-02 13:36:10 +00:00
|
|
|
},
|
2021-01-27 12:19:09 +00:00
|
|
|
],
|
|
|
|
}],
|
2020-11-02 13:36:10 +00:00
|
|
|
|
|
|
|
buttons: [
|
|
|
|
{
|
2021-01-27 12:19:09 +00:00
|
|
|
handler: 'loginTFA',
|
|
|
|
reference: 'tfaButton',
|
|
|
|
disabled: true,
|
|
|
|
bind: {
|
2021-02-01 10:48:33 +00:00
|
|
|
text: '{confirmText}',
|
2021-01-27 12:19:09 +00:00
|
|
|
disabled: '{!canConfirm}',
|
|
|
|
},
|
2020-11-02 13:36:10 +00:00
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|