]>
git.proxmox.com Git - proxmox-backup.git/blob - www/LoginView.js
ae443e0f08cb95ee83b3224bf22b1e276798ceb6
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',
258 availabelChallenge
: {},
265 xclass
: 'Ext.app.ViewController',
267 init: function(view
) {
269 let vm
= me
.getViewModel();
272 throw "no userid given";
275 throw "no ticket given";
277 const challenge
= view
.challenge
;
279 throw "no challenge given";
282 let lastTabId
= me
.getLastTabUsed();
283 let initialTab
= -1, i
= 0;
284 for (const k
of ['webauthn', 'totp', 'recovery']) {
285 const available
= !!challenge
[k
];
286 vm
.set(`availabelChallenge.${k}`, available
);
289 if (i
=== lastTabId
) {
291 } else if (initialTab
< 0) {
297 view
.down('tabpanel').setActiveTab(initialTab
);
299 if (challenge
.recovery
) {
300 me
.lookup('availableRecovery').update(Ext
.String
.htmlEncode(
301 gettext('Available recovery keys: ') + view
.challenge
.recovery
.join(', '),
303 me
.lookup('availableRecovery').setVisible(true);
304 if (view
.challenge
.recovery
.length
<= 3) {
305 me
.lookup('recoveryLow').setVisible(true);
309 if (challenge
.webauthn
) {
310 let _promise
= me
.loginWebauthn();
315 tabchange: function(tabPanel
, newCard
, oldCard
) {
316 // for now every TFA method has at max one field, so keep it simple..
317 let oldField
= oldCard
.down('field');
319 oldField
.setDisabled(true);
321 let newField
= newCard
.down('field');
323 newField
.setDisabled(false);
327 this.saveLastTabUsed(tabPanel
, newCard
);
331 validitychange: function(field
, valid
) {
332 // triggers only for enabled fields and we disable the one from the
333 // non-visible tab, so we can just directly use the valid param
334 this.getViewModel().set('canConfirm', valid
);
339 saveLastTabUsed: function(tabPanel
, card
) {
340 let id
= tabPanel
.items
.indexOf(card
);
341 window
.localStorage
.setItem('PBS.TFALogin.lastTab', JSON
.stringify({ id
}));
344 getLastTabUsed: function() {
345 let data
= window
.localStorage
.getItem('PBS.TFALogin.lastTab');
346 if (typeof data
=== 'string') {
347 let last
= JSON
.parse(data
);
353 onClose: function() {
355 let view
= me
.getView();
357 if (!view
.cancelled
) {
365 this.getView().close();
368 loginTotp: function() {
371 let code
= me
.lookup('totp').getValue();
372 let _promise
= me
.finishChallenge(`totp:${code}`);
375 loginWebauthn
: async
function() {
377 let view
= me
.getView();
379 me
.lookup('webAuthnWaiting').setVisible(true);
381 let challenge
= view
.challenge
.webauthn
;
383 // Byte array fixup, keep challenge string:
384 let challenge_str
= challenge
.publicKey
.challenge
;
385 challenge
.publicKey
.challenge
= PBS
.Utils
.base64url_to_bytes(challenge_str
);
386 for (const cred
of challenge
.publicKey
.allowCredentials
) {
387 cred
.id
= PBS
.Utils
.base64url_to_bytes(cred
.id
);
390 let controller
= new AbortController();
391 challenge
.signal
= controller
.signal
;
396 hwrsp
= await navigator
.credentials
.get(challenge
);
398 view
.onReject(error
);
401 let waitingMessage
= me
.lookup('webAuthnWaiting');
402 if (waitingMessage
) {
403 waitingMessage
.setVisible(false);
410 challenge
: challenge_str
,
411 rawId
: PBS
.Utils
.bytes_to_base64url(hwrsp
.rawId
),
413 authenticatorData
: PBS
.Utils
.bytes_to_base64url(
414 hwrsp
.response
.authenticatorData
,
416 clientDataJSON
: PBS
.Utils
.bytes_to_base64url(hwrsp
.response
.clientDataJSON
),
417 signature
: PBS
.Utils
.bytes_to_base64url(hwrsp
.response
.signature
),
421 await me
.finishChallenge("webauthn:" + JSON
.stringify(response
));
424 loginRecovery: function() {
427 let key
= me
.lookup('recoveryKey').getValue();
428 let _promise
= me
.finishChallenge(`recovery:${key}`);
431 loginTFA: function() {
433 let view
= me
.getView();
434 let tfaPanel
= view
.down('tabpanel').getActiveTab();
435 me
[tfaPanel
.handler
]();
438 finishChallenge: function(password
) {
440 let view
= me
.getView();
441 view
.cancelled
= false;
444 username
: view
.userid
,
445 'tfa-challenge': view
.ticket
,
449 let resolve
= view
.onResolve
;
450 let reject
= view
.onReject
;
453 return PBS
.Async
.api2({
454 url
: '/api2/extjs/access/ticket',
472 stateId
: 'pbs-tfa-login-panel', // FIXME: do manually - get/setState miss
474 stateEvents
: ['tabchange'],
479 iconCls
: 'fa fa-fw fa-shield',
480 handler
: 'loginWebauthn',
482 disabled
: '{!availabelChallenge.webauthn}',
487 html
: `<i class="fa fa-refresh fa-spin fa-fw"></i>` +
488 gettext('Please insert your authenticator device and press its button'),
492 html
: gettext('Waiting for second factor.'),
493 reference
: 'webAuthnWaiting',
500 title
: gettext('TOTP App'),
501 iconCls
: 'fa fa-fw fa-clock-o',
502 handler
: 'loginTotp',
504 disabled
: '{!availabelChallenge.totp}',
509 fieldLabel
: gettext('Please enter your TOTP verification code'),
516 regexText
: 'TOTP codes consist of six decimal digits',
522 title
: gettext('Recovery Key'),
523 iconCls
: 'fa fa-fw fa-file-text-o',
524 handler
: 'loginRecovery',
526 disabled
: '{!availabelChallenge.recovery}',
531 reference
: 'availableRecovery',
536 fieldLabel
: gettext('Please enter one of your single-use recovery keys'),
540 reference
: 'recoveryKey',
542 regex
: /^[0-9a-f]{4}(-[0-9a-f]{4}){3}$/,
543 regexText
: 'Does not looks like a valid recovery key',
547 reference
: 'recoveryInfo',
548 hidden
: true, // FIXME: remove this?
549 html
: gettext('Note that each recovery code can only be used once!'),
553 reference
: 'recoveryLow',
555 html
: '<i class="fa fa-exclamation-triangle warning"></i>'
556 + gettext('Less than {0} recovery keys available. Please generate a new set!'),
565 text
: gettext('Confirm Second Factor'),
567 reference
: 'tfaButton',
570 disabled
: '{!canConfirm}',