]>
git.proxmox.com Git - proxmox-backup.git/blob - www/LoginView.js
f73803392bbbd4fe90a27ba76c6077be64135570
1 Ext
.define('PBS.LoginView', {
2 extend
: 'Ext.container.Container',
6 xclass
: 'Ext.app.ViewController',
8 submitForm
: async
function() {
10 var loginForm
= me
.lookupReference('loginForm');
11 var unField
= me
.lookupReference('usernameField');
12 var saveunField
= me
.lookupReference('saveunField');
14 if (!loginForm
.isValid()) {
18 let params
= loginForm
.getValues();
20 params
.username
= params
.username
+ '@' + params
.realm
;
23 if (loginForm
.isVisible()) {
24 loginForm
.mask(gettext('Please wait...'), 'x-mask-loading');
27 // set or clear username
28 var sp
= Ext
.state
.Manager
.getProvider();
29 if (saveunField
.getValue() === true) {
30 sp
.set(unField
.getStateId(), unField
.getValue());
32 sp
.clear(unField
.getStateId());
34 sp
.set(saveunField
.getStateId(), saveunField
.getValue());
37 let resp
= await PBS
.Async
.api2({
38 url
: '/api2/extjs/access/ticket',
43 let data
= resp
.result
.data
;
44 if (data
.ticket
.startsWith("PBS:!tfa!")) {
45 data
= await me
.performTFAChallenge(data
);
48 PBS
.Utils
.updateLoginData(data
);
49 PBS
.app
.changeView('mainview');
51 Proxmox
.Utils
.authClear();
55 gettext('Login failed. Please try again'),
60 performTFAChallenge
: async
function(data
) {
63 let userid
= data
.username
;
64 let ticket
= data
.ticket
;
65 let challenge
= JSON
.parse(decodeURIComponent(
66 ticket
.split(':')[1].slice("!tfa!".length
),
69 let resp
= await
new Promise((resolve
, reject
) => {
70 Ext
.create('PBS.login.TfaWindow', {
74 onResolve
: value
=> resolve(value
),
79 return resp
.result
.data
;
83 'field[name=username]': {
84 specialkey: function(f
, e
) {
85 if (e
.getKey() === e
.ENTER
) {
86 var pf
= this.lookupReference('passwordField');
94 change: function(f
, value
) {
95 var dt
= Ext
.Date
.add(new Date(), Ext
.Date
.YEAR
, 10);
96 Ext
.util
.Cookies
.set('PBSLangCookie', value
, dt
);
97 this.getView().mask(gettext('Please wait...'), 'x-mask-loading');
98 window
.location
.reload();
101 'button[reference=loginButton]': {
104 'window[reference=loginwindow]': {
106 var sp
= Ext
.state
.Manager
.getProvider();
107 var checkboxField
= this.lookupReference('saveunField');
108 var unField
= this.lookupReference('usernameField');
110 var checked
= sp
.get(checkboxField
.getStateId());
111 checkboxField
.setValue(checked
);
113 if (checked
=== true) {
114 var username
= sp
.get(unField
.getStateId());
115 unField
.setValue(username
);
116 var pwField
= this.lookupReference('passwordField');
142 xtype
: 'proxmoxlogo',
146 xtype
: 'versioninfo',
158 reference
: 'loginwindow',
163 defaultFocus
: 'usernameField',
169 title
: gettext('Proxmox Backup Server Login'),
177 defaultButton
: 'loginButton',
178 url
: '/api2/extjs/access/ticket',
179 reference
: 'loginForm',
189 fieldLabel
: gettext('User name'),
191 itemId
: 'usernameField',
192 reference
: 'usernameField',
193 stateId
: 'login-username',
197 inputType
: 'password',
198 fieldLabel
: gettext('Password'),
200 itemId
: 'passwordField',
201 reference
: 'passwordField',
204 xtype
: 'pmxRealmComboBox',
208 xtype
: 'proxmoxLanguageSelector',
209 fieldLabel
: gettext('Language'),
210 value
: Ext
.util
.Cookies
.get('PBSLangCookie') || Proxmox
.defaultLang
|| 'en',
212 reference
: 'langField',
219 fieldLabel
: gettext('Save User name'),
220 name
: 'saveusername',
221 reference
: 'saveunField',
222 stateId
: 'login-saveusername',
228 text
: gettext('Login'),
229 reference
: 'loginButton',
239 Ext
.define('PBS.login.TfaWindow', {
240 extend
: 'Ext.window.Window',
241 mixins
: ['Proxmox.Mixin.CBind'],
243 title
: gettext("Second login factor required"),
253 defaultButton
: 'tfaButton',
257 confirmText
: gettext('Confirm Second Factor'),
259 availabelChallenge
: {},
266 xclass
: 'Ext.app.ViewController',
268 init: function(view
) {
270 let vm
= me
.getViewModel();
273 throw "no userid given";
276 throw "no ticket given";
278 const challenge
= view
.challenge
;
280 throw "no challenge given";
283 let lastTabId
= me
.getLastTabUsed();
284 let initialTab
= -1, i
= 0;
285 for (const k
of ['webauthn', 'totp', 'recovery']) {
286 const available
= !!challenge
[k
];
287 vm
.set(`availabelChallenge.${k}`, available
);
290 if (i
=== lastTabId
) {
292 } else if (initialTab
< 0) {
298 view
.down('tabpanel').setActiveTab(initialTab
);
300 if (challenge
.recovery
) {
301 me
.lookup('availableRecovery').update(Ext
.String
.htmlEncode(
302 gettext('Available recovery keys: ') + view
.challenge
.recovery
.join(', '),
304 me
.lookup('availableRecovery').setVisible(true);
305 if (view
.challenge
.recovery
.length
<= 3) {
306 me
.lookup('recoveryLow').setVisible(true);
310 if (challenge
.webauthn
&& initialTab
=== 0) {
311 let _promise
= me
.loginWebauthn();
316 tabchange: function(tabPanel
, newCard
, oldCard
) {
317 // for now every TFA method has at max one field, so keep it simple..
318 let oldField
= oldCard
.down('field');
320 oldField
.setDisabled(true);
322 let newField
= newCard
.down('field');
324 newField
.setDisabled(false);
329 let confirmText
= newCard
.confirmText
|| gettext('Confirm Second Factor');
330 this.getViewModel().set('confirmText', confirmText
);
332 this.saveLastTabUsed(tabPanel
, newCard
);
336 validitychange: function(field
, valid
) {
337 // triggers only for enabled fields and we disable the one from the
338 // non-visible tab, so we can just directly use the valid param
339 this.getViewModel().set('canConfirm', valid
);
344 saveLastTabUsed: function(tabPanel
, card
) {
345 let id
= tabPanel
.items
.indexOf(card
);
346 window
.localStorage
.setItem('PBS.TFALogin.lastTab', JSON
.stringify({ id
}));
349 getLastTabUsed: function() {
350 let data
= window
.localStorage
.getItem('PBS.TFALogin.lastTab');
351 if (typeof data
=== 'string') {
352 let last
= JSON
.parse(data
);
358 onClose: function() {
360 let view
= me
.getView();
362 if (!view
.cancelled
) {
370 this.getView().close();
373 loginTotp: function() {
376 let code
= me
.lookup('totp').getValue();
377 let _promise
= me
.finishChallenge(`totp:${code}`);
380 loginWebauthn
: async
function() {
382 let view
= me
.getView();
384 me
.lookup('webAuthnWaiting').setVisible(true);
386 let challenge
= view
.challenge
.webauthn
;
388 if (typeof challenge
.string
!== 'string') {
389 // Byte array fixup, keep challenge string:
390 challenge
.string
= challenge
.publicKey
.challenge
;
391 challenge
.publicKey
.challenge
= PBS
.Utils
.base64url_to_bytes(challenge
.string
);
392 for (const cred
of challenge
.publicKey
.allowCredentials
) {
393 cred
.id
= PBS
.Utils
.base64url_to_bytes(cred
.id
);
397 let controller
= new AbortController();
398 challenge
.signal
= controller
.signal
;
403 hwrsp
= await navigator
.credentials
.get(challenge
);
405 // we do NOT want to fail login because of canceling the challenge actively,
406 // in some browser that's the only way to switch over to another method as the
407 // disallow user input during the time the challenge is active
408 // checking for error.code === DOMException.ABORT_ERR only works in firefox -.-
409 this.getViewModel().set('canConfirm', true);
410 // FIXME: better handling, show some message, ...?
413 let waitingMessage
= me
.lookup('webAuthnWaiting');
414 if (waitingMessage
) {
415 waitingMessage
.setVisible(false);
422 challenge
: challenge
.string
,
423 rawId
: PBS
.Utils
.bytes_to_base64url(hwrsp
.rawId
),
425 authenticatorData
: PBS
.Utils
.bytes_to_base64url(
426 hwrsp
.response
.authenticatorData
,
428 clientDataJSON
: PBS
.Utils
.bytes_to_base64url(hwrsp
.response
.clientDataJSON
),
429 signature
: PBS
.Utils
.bytes_to_base64url(hwrsp
.response
.signature
),
433 await me
.finishChallenge("webauthn:" + JSON
.stringify(response
));
436 loginRecovery: function() {
439 let key
= me
.lookup('recoveryKey').getValue();
440 let _promise
= me
.finishChallenge(`recovery:${key}`);
443 loginTFA: function() {
445 // avoid triggering more than once during challenge
446 me
.getViewModel().set('canConfirm', false);
447 let view
= me
.getView();
448 let tfaPanel
= view
.down('tabpanel').getActiveTab();
449 me
[tfaPanel
.handler
]();
452 finishChallenge: function(password
) {
454 let view
= me
.getView();
455 view
.cancelled
= false;
458 username
: view
.userid
,
459 'tfa-challenge': view
.ticket
,
463 let resolve
= view
.onResolve
;
464 let reject
= view
.onReject
;
467 return PBS
.Async
.api2({
468 url
: '/api2/extjs/access/ticket',
490 iconCls
: 'fa fa-fw fa-shield',
491 confirmText
: gettext('Start WebAuthn challenge'),
492 handler
: 'loginWebauthn',
494 disabled
: '{!availabelChallenge.webauthn}',
499 html
: gettext('Please insert your authentication device and press its button'),
503 html
: gettext('Waiting for second factor.') +`<i class="fa fa-refresh fa-spin fa-fw"></i>`,
504 reference
: 'webAuthnWaiting',
511 title
: gettext('TOTP App'),
512 iconCls
: 'fa fa-fw fa-clock-o',
513 handler
: 'loginTotp',
515 disabled
: '{!availabelChallenge.totp}',
520 fieldLabel
: gettext('Please enter your TOTP verification code'),
527 regexText
: 'TOTP codes consist of six decimal digits',
533 title
: gettext('Recovery Key'),
534 iconCls
: 'fa fa-fw fa-file-text-o',
535 handler
: 'loginRecovery',
537 disabled
: '{!availabelChallenge.recovery}',
542 reference
: 'availableRecovery',
547 fieldLabel
: gettext('Please enter one of your single-use recovery keys'),
551 reference
: 'recoveryKey',
553 regex
: /^[0-9a-f]{4}(-[0-9a-f]{4}){3}$/,
554 regexText
: 'Does not looks like a valid recovery key',
558 reference
: 'recoveryLow',
560 html
: '<i class="fa fa-exclamation-triangle warning"></i>'
561 + gettext('Less than {0} recovery keys available. Please generate a new set after login!'),
571 reference
: 'tfaButton',
574 text
: '{confirmText}',
575 disabled
: '{!canConfirm}',