]>
Commit | Line | Data |
---|---|---|
34f956bc DM |
1 | Ext.define('PBS.LoginView', { |
2 | extend: 'Ext.container.Container', | |
3 | xtype: 'loginview', | |
4 | ||
cd975e57 DM |
5 | viewModel: { |
6 | data: { | |
7 | openid: false, | |
8 | }, | |
9 | formulas: { | |
10 | button_text: function(get) { | |
11 | if (get("openid") === true) { | |
12 | return gettext("Login (OpenID redirect)"); | |
13 | } else { | |
14 | return gettext("Login"); | |
15 | } | |
16 | }, | |
17 | }, | |
18 | }, | |
19 | ||
34f956bc DM |
20 | controller: { |
21 | xclass: 'Ext.app.ViewController', | |
22 | ||
fbeac4ea | 23 | submitForm: async function() { |
34f956bc | 24 | var me = this; |
34f956bc | 25 | var loginForm = me.lookupReference('loginForm'); |
9abcae1b DM |
26 | var unField = me.lookupReference('usernameField'); |
27 | var saveunField = me.lookupReference('saveunField'); | |
34f956bc | 28 | |
9abcae1b DM |
29 | if (!loginForm.isValid()) { |
30 | return; | |
31 | } | |
32 | ||
e1d85f18 | 33 | let creds = loginForm.getValues(); |
9abcae1b | 34 | |
cd975e57 | 35 | if (this.getViewModel().data.openid === true) { |
e1d85f18 | 36 | const redirectURL = location.origin; |
cd975e57 | 37 | try { |
3006d70e | 38 | let resp = await Proxmox.Async.api2({ |
cd975e57 DM |
39 | url: '/api2/extjs/access/openid/auth-url', |
40 | params: { | |
e1d85f18 TL |
41 | realm: creds.realm, |
42 | "redirect-url": redirectURL, | |
cd975e57 DM |
43 | }, |
44 | method: 'POST', | |
45 | }); | |
46 | window.location = resp.result.data; | |
3006d70e | 47 | } catch (response) { |
cd975e57 DM |
48 | Proxmox.Utils.authClear(); |
49 | loginForm.unmask(); | |
50 | Ext.MessageBox.alert( | |
51 | gettext('Error'), | |
3006d70e | 52 | gettext('OpenID redirect failed, please try again') + `<br>${response.result.message}`, |
cd975e57 DM |
53 | ); |
54 | } | |
55 | return; | |
56 | } | |
57 | ||
e1d85f18 TL |
58 | creds.username = `${creds.username}@${creds.realm}`; |
59 | delete creds.realm; | |
9abcae1b DM |
60 | |
61 | if (loginForm.isVisible()) { | |
62 | loginForm.mask(gettext('Please wait...'), 'x-mask-loading'); | |
34f956bc | 63 | } |
9abcae1b DM |
64 | |
65 | // set or clear username | |
66 | var sp = Ext.state.Manager.getProvider(); | |
67 | if (saveunField.getValue() === true) { | |
68 | sp.set(unField.getStateId(), unField.getValue()); | |
69 | } else { | |
70 | sp.clear(unField.getStateId()); | |
71 | } | |
72 | sp.set(saveunField.getStateId(), saveunField.getValue()); | |
73 | ||
fbeac4ea | 74 | try { |
3006d70e | 75 | let resp = await Proxmox.Async.api2({ |
fbeac4ea | 76 | url: '/api2/extjs/access/ticket', |
e1d85f18 | 77 | params: creds, |
fbeac4ea WB |
78 | method: 'POST', |
79 | }); | |
80 | ||
81 | let data = resp.result.data; | |
82 | if (data.ticket.startsWith("PBS:!tfa!")) { | |
83 | data = await me.performTFAChallenge(data); | |
84 | } | |
85 | ||
86 | PBS.Utils.updateLoginData(data); | |
87 | PBS.app.changeView('mainview'); | |
88 | } catch (error) { | |
fbeac4ea WB |
89 | Proxmox.Utils.authClear(); |
90 | loginForm.unmask(); | |
91 | Ext.MessageBox.alert( | |
92 | gettext('Error'), | |
93 | gettext('Login failed. Please try again'), | |
94 | ); | |
95 | } | |
96 | }, | |
97 | ||
98 | performTFAChallenge: async function(data) { | |
99 | let me = this; | |
100 | ||
101 | let userid = data.username; | |
102 | let ticket = data.ticket; | |
103 | let challenge = JSON.parse(decodeURIComponent( | |
104 | ticket.split(':')[1].slice("!tfa!".length), | |
105 | )); | |
106 | ||
107 | let resp = await new Promise((resolve, reject) => { | |
108 | Ext.create('PBS.login.TfaWindow', { | |
109 | userid, | |
110 | ticket, | |
111 | challenge, | |
112 | onResolve: value => resolve(value), | |
113 | onReject: reject, | |
114 | }).show(); | |
9abcae1b | 115 | }); |
fbeac4ea WB |
116 | |
117 | return resp.result.data; | |
34f956bc DM |
118 | }, |
119 | ||
120 | control: { | |
9abcae1b DM |
121 | 'field[name=username]': { |
122 | specialkey: function(f, e) { | |
123 | if (e.getKey() === e.ENTER) { | |
124 | var pf = this.lookupReference('passwordField'); | |
125 | if (!pf.getValue()) { | |
126 | pf.focus(false); | |
127 | } | |
128 | } | |
8acd4d9a | 129 | }, |
9abcae1b DM |
130 | }, |
131 | 'field[name=lang]': { | |
132 | change: function(f, value) { | |
133 | var dt = Ext.Date.add(new Date(), Ext.Date.YEAR, 10); | |
134 | Ext.util.Cookies.set('PBSLangCookie', value, dt); | |
135 | this.getView().mask(gettext('Please wait...'), 'x-mask-loading'); | |
136 | window.location.reload(); | |
8acd4d9a | 137 | }, |
9abcae1b | 138 | }, |
cd975e57 DM |
139 | 'field[name=realm]': { |
140 | change: function(f, value) { | |
141 | let record = f.store.getById(value); | |
142 | if (record === undefined) return; | |
143 | let data = record.data; | |
144 | this.getViewModel().set("openid", data.type === "openid"); | |
145 | }, | |
146 | }, | |
34f956bc | 147 | 'button[reference=loginButton]': { |
8acd4d9a | 148 | click: 'submitForm', |
9abcae1b DM |
149 | }, |
150 | 'window[reference=loginwindow]': { | |
151 | show: function() { | |
152 | var sp = Ext.state.Manager.getProvider(); | |
153 | var checkboxField = this.lookupReference('saveunField'); | |
154 | var unField = this.lookupReference('usernameField'); | |
155 | ||
156 | var checked = sp.get(checkboxField.getStateId()); | |
157 | checkboxField.setValue(checked); | |
158 | ||
8acd4d9a | 159 | if (checked === true) { |
9abcae1b DM |
160 | var username = sp.get(unField.getStateId()); |
161 | unField.setValue(username); | |
162 | var pwField = this.lookupReference('passwordField'); | |
163 | pwField.focus(); | |
164 | } | |
cd975e57 | 165 | |
e1d85f18 TL |
166 | let auth = Proxmox.Utils.getOpenIDRedirectionAuthorization(); |
167 | if (auth !== undefined) { | |
cd975e57 DM |
168 | Proxmox.Utils.authClear(); |
169 | ||
170 | let loginForm = this.lookupReference('loginForm'); | |
171 | loginForm.mask(gettext('OpenID login - please wait...'), 'x-mask-loading'); | |
172 | ||
e1d85f18 TL |
173 | // openID checks the original redirection URL we used, so pass that too |
174 | const redirectURL = location.origin; | |
cd975e57 DM |
175 | |
176 | Proxmox.Utils.API2Request({ | |
177 | url: '/api2/extjs/access/openid/login', | |
178 | params: { | |
e1d85f18 TL |
179 | state: auth.state, |
180 | code: auth.code, | |
181 | "redirect-url": redirectURL, | |
cd975e57 DM |
182 | }, |
183 | method: 'POST', | |
184 | failure: function(response) { | |
185 | loginForm.unmask(); | |
85b6c4ea | 186 | let error = response.htmlStatus; |
cd975e57 DM |
187 | Ext.MessageBox.alert( |
188 | gettext('Error'), | |
85b6c4ea TL |
189 | gettext('OpenID login failed, please try again') + `<br>${error}`, |
190 | () => { window.location = redirectURL; }, | |
cd975e57 DM |
191 | ); |
192 | }, | |
193 | success: function(response, options) { | |
194 | loginForm.unmask(); | |
e1d85f18 TL |
195 | let creds = response.result.data; |
196 | PBS.Utils.updateLoginData(creds); | |
cd975e57 | 197 | PBS.app.changeView('mainview'); |
e1d85f18 | 198 | history.replaceState(null, '', `${redirectURL}#pbsDashboard`); |
cd975e57 DM |
199 | }, |
200 | }); | |
201 | } | |
8acd4d9a TL |
202 | }, |
203 | }, | |
204 | }, | |
34f956bc DM |
205 | }, |
206 | ||
207 | plugins: 'viewport', | |
208 | ||
209 | layout: { | |
8acd4d9a | 210 | type: 'border', |
34f956bc DM |
211 | }, |
212 | ||
213 | items: [ | |
214 | { | |
215 | region: 'north', | |
216 | xtype: 'container', | |
217 | layout: { | |
218 | type: 'hbox', | |
8acd4d9a | 219 | align: 'middle', |
34f956bc DM |
220 | }, |
221 | margin: '2 5 2 5', | |
222 | height: 38, | |
223 | items: [ | |
224 | { | |
1d8ef0dc DC |
225 | xtype: 'proxmoxlogo', |
226 | prefix: '', | |
34f956bc DM |
227 | }, |
228 | { | |
229 | xtype: 'versioninfo', | |
8acd4d9a TL |
230 | makeApiCall: false, |
231 | }, | |
232 | ], | |
34f956bc DM |
233 | }, |
234 | { | |
8acd4d9a | 235 | region: 'center', |
34f956bc DM |
236 | }, |
237 | { | |
238 | xtype: 'window', | |
239 | closable: false, | |
240 | resizable: false, | |
241 | reference: 'loginwindow', | |
242 | autoShow: true, | |
243 | modal: true, | |
9abcae1b | 244 | width: 400, |
34f956bc | 245 | |
9abcae1b | 246 | defaultFocus: 'usernameField', |
34f956bc DM |
247 | |
248 | layout: { | |
8acd4d9a | 249 | type: 'auto', |
34f956bc DM |
250 | }, |
251 | ||
252 | title: gettext('Proxmox Backup Server Login'), | |
253 | ||
254 | items: [ | |
255 | { | |
256 | xtype: 'form', | |
257 | layout: { | |
8acd4d9a | 258 | type: 'form', |
34f956bc DM |
259 | }, |
260 | defaultButton: 'loginButton', | |
261 | url: '/api2/extjs/access/ticket', | |
262 | reference: 'loginForm', | |
263 | ||
264 | fieldDefaults: { | |
265 | labelAlign: 'right', | |
8acd4d9a | 266 | allowBlank: false, |
34f956bc DM |
267 | }, |
268 | ||
269 | items: [ | |
270 | { | |
271 | xtype: 'textfield', | |
272 | fieldLabel: gettext('User name'), | |
273 | name: 'username', | |
274 | itemId: 'usernameField', | |
9abcae1b | 275 | reference: 'usernameField', |
8acd4d9a | 276 | stateId: 'login-username', |
cd975e57 DM |
277 | bind: { |
278 | visible: "{!openid}", | |
279 | disabled: "{openid}", | |
280 | }, | |
34f956bc DM |
281 | }, |
282 | { | |
283 | xtype: 'textfield', | |
284 | inputType: 'password', | |
285 | fieldLabel: gettext('Password'), | |
286 | name: 'password', | |
3a841004 TL |
287 | itemId: 'passwordField', |
288 | reference: 'passwordField', | |
cd975e57 DM |
289 | bind: { |
290 | visible: "{!openid}", | |
291 | disabled: "{openid}", | |
292 | }, | |
9abcae1b | 293 | }, |
1d8ef0dc DC |
294 | { |
295 | xtype: 'pmxRealmComboBox', | |
8acd4d9a | 296 | name: 'realm', |
1d8ef0dc | 297 | }, |
9abcae1b DM |
298 | { |
299 | xtype: 'proxmoxLanguageSelector', | |
300 | fieldLabel: gettext('Language'), | |
301 | value: Ext.util.Cookies.get('PBSLangCookie') || Proxmox.defaultLang || 'en', | |
302 | name: 'lang', | |
303 | reference: 'langField', | |
8acd4d9a TL |
304 | submitValue: false, |
305 | }, | |
34f956bc DM |
306 | ], |
307 | buttons: [ | |
9abcae1b DM |
308 | { |
309 | xtype: 'checkbox', | |
310 | fieldLabel: gettext('Save User name'), | |
311 | name: 'saveusername', | |
312 | reference: 'saveunField', | |
313 | stateId: 'login-saveusername', | |
314 | labelWidth: 250, | |
315 | labelAlign: 'right', | |
8acd4d9a | 316 | submitValue: false, |
cd975e57 DM |
317 | bind: { |
318 | visible: "{!openid}", | |
319 | }, | |
9abcae1b | 320 | }, |
34f956bc | 321 | { |
cd975e57 DM |
322 | bind: { |
323 | text: "{button_text}", | |
324 | }, | |
34f956bc | 325 | reference: 'loginButton', |
8acd4d9a TL |
326 | formBind: true, |
327 | }, | |
328 | ], | |
329 | }, | |
330 | ], | |
331 | }, | |
332 | ], | |
34f956bc | 333 | }); |
fbeac4ea WB |
334 | |
335 | Ext.define('PBS.login.TfaWindow', { | |
336 | extend: 'Ext.window.Window', | |
337 | mixins: ['Proxmox.Mixin.CBind'], | |
338 | ||
fbeac4ea WB |
339 | title: gettext("Second login factor required"), |
340 | ||
f91481ed TL |
341 | modal: true, |
342 | resizable: false, | |
fbeac4ea WB |
343 | width: 512, |
344 | layout: { | |
345 | type: 'vbox', | |
346 | align: 'stretch', | |
347 | }, | |
348 | ||
f91481ed TL |
349 | defaultButton: 'tfaButton', |
350 | ||
351 | viewModel: { | |
352 | data: { | |
2dbc1a9a | 353 | confirmText: gettext('Confirm Second Factor'), |
f91481ed | 354 | canConfirm: false, |
54067d82 | 355 | availableChallenge: {}, |
f91481ed TL |
356 | }, |
357 | }, | |
358 | ||
359 | cancelled: true, | |
fbeac4ea | 360 | |
fbeac4ea WB |
361 | controller: { |
362 | xclass: 'Ext.app.ViewController', | |
363 | ||
364 | init: function(view) { | |
365 | let me = this; | |
f91481ed | 366 | let vm = me.getViewModel(); |
fbeac4ea WB |
367 | |
368 | if (!view.userid) { | |
369 | throw "no userid given"; | |
370 | } | |
fbeac4ea WB |
371 | if (!view.ticket) { |
372 | throw "no ticket given"; | |
373 | } | |
f91481ed TL |
374 | const challenge = view.challenge; |
375 | if (!challenge) { | |
fbeac4ea WB |
376 | throw "no challenge given"; |
377 | } | |
378 | ||
6ee85d57 TL |
379 | let lastTabId = me.getLastTabUsed(); |
380 | let initialTab = -1, i = 0; | |
f91481ed TL |
381 | for (const k of ['webauthn', 'totp', 'recovery']) { |
382 | const available = !!challenge[k]; | |
54067d82 | 383 | vm.set(`availableChallenge.${k}`, available); |
fbeac4ea | 384 | |
6ee85d57 TL |
385 | if (available) { |
386 | if (i === lastTabId) { | |
387 | initialTab = i; | |
388 | } else if (initialTab < 0) { | |
389 | initialTab = i; | |
390 | } | |
f91481ed TL |
391 | } |
392 | i++; | |
fbeac4ea | 393 | } |
6ee85d57 | 394 | view.down('tabpanel').setActiveTab(initialTab); |
fbeac4ea | 395 | |
f91481ed TL |
396 | if (challenge.recovery) { |
397 | me.lookup('availableRecovery').update(Ext.String.htmlEncode( | |
398 | gettext('Available recovery keys: ') + view.challenge.recovery.join(', '), | |
399 | )); | |
400 | me.lookup('availableRecovery').setVisible(true); | |
401 | if (view.challenge.recovery.length <= 3) { | |
402 | me.lookup('recoveryLow').setVisible(true); | |
403 | } | |
fbeac4ea WB |
404 | } |
405 | ||
a11c8ab4 | 406 | if (challenge.webauthn && initialTab === 0) { |
fbeac4ea WB |
407 | let _promise = me.loginWebauthn(); |
408 | } | |
409 | }, | |
f91481ed TL |
410 | control: { |
411 | 'tabpanel': { | |
412 | tabchange: function(tabPanel, newCard, oldCard) { | |
413 | // for now every TFA method has at max one field, so keep it simple.. | |
414 | let oldField = oldCard.down('field'); | |
415 | if (oldField) { | |
416 | oldField.setDisabled(true); | |
417 | } | |
418 | let newField = newCard.down('field'); | |
419 | if (newField) { | |
420 | newField.setDisabled(false); | |
421 | newField.focus(); | |
422 | newField.validate(); | |
423 | } | |
2dbc1a9a TL |
424 | |
425 | let confirmText = newCard.confirmText || gettext('Confirm Second Factor'); | |
426 | this.getViewModel().set('confirmText', confirmText); | |
427 | ||
6ee85d57 | 428 | this.saveLastTabUsed(tabPanel, newCard); |
f91481ed TL |
429 | }, |
430 | }, | |
431 | 'field': { | |
432 | validitychange: function(field, valid) { | |
433 | // triggers only for enabled fields and we disable the one from the | |
434 | // non-visible tab, so we can just directly use the valid param | |
435 | this.getViewModel().set('canConfirm', valid); | |
436 | }, | |
b5c60881 | 437 | afterrender: field => field.focus(), // ensure focus after initial render |
f91481ed TL |
438 | }, |
439 | }, | |
fbeac4ea | 440 | |
6ee85d57 TL |
441 | saveLastTabUsed: function(tabPanel, card) { |
442 | let id = tabPanel.items.indexOf(card); | |
443 | window.localStorage.setItem('PBS.TFALogin.lastTab', JSON.stringify({ id })); | |
444 | }, | |
445 | ||
446 | getLastTabUsed: function() { | |
447 | let data = window.localStorage.getItem('PBS.TFALogin.lastTab'); | |
448 | if (typeof data === 'string') { | |
449 | let last = JSON.parse(data); | |
450 | return last.id; | |
451 | } | |
452 | return null; | |
453 | }, | |
454 | ||
fbeac4ea WB |
455 | onClose: function() { |
456 | let me = this; | |
457 | let view = me.getView(); | |
458 | ||
459 | if (!view.cancelled) { | |
460 | return; | |
461 | } | |
462 | ||
463 | view.onReject(); | |
464 | }, | |
465 | ||
466 | cancel: function() { | |
467 | this.getView().close(); | |
468 | }, | |
469 | ||
470 | loginTotp: function() { | |
471 | let me = this; | |
472 | ||
f91481ed TL |
473 | let code = me.lookup('totp').getValue(); |
474 | let _promise = me.finishChallenge(`totp:${code}`); | |
fbeac4ea WB |
475 | }, |
476 | ||
477 | loginWebauthn: async function() { | |
478 | let me = this; | |
479 | let view = me.getView(); | |
480 | ||
f91481ed | 481 | me.lookup('webAuthnWaiting').setVisible(true); |
a8a01327 | 482 | me.lookup('webAuthnError').setVisible(false); |
fbeac4ea WB |
483 | |
484 | let challenge = view.challenge.webauthn; | |
485 | ||
e90fdf5b TL |
486 | if (typeof challenge.string !== 'string') { |
487 | // Byte array fixup, keep challenge string: | |
488 | challenge.string = challenge.publicKey.challenge; | |
489 | challenge.publicKey.challenge = PBS.Utils.base64url_to_bytes(challenge.string); | |
490 | for (const cred of challenge.publicKey.allowCredentials) { | |
491 | cred.id = PBS.Utils.base64url_to_bytes(cred.id); | |
492 | } | |
fbeac4ea WB |
493 | } |
494 | ||
f91481ed TL |
495 | let controller = new AbortController(); |
496 | challenge.signal = controller.signal; | |
497 | ||
fbeac4ea WB |
498 | let hwrsp; |
499 | try { | |
f91481ed | 500 | //Promise.race( ... |
fbeac4ea WB |
501 | hwrsp = await navigator.credentials.get(challenge); |
502 | } catch (error) { | |
e90fdf5b TL |
503 | // we do NOT want to fail login because of canceling the challenge actively, |
504 | // in some browser that's the only way to switch over to another method as the | |
505 | // disallow user input during the time the challenge is active | |
506 | // checking for error.code === DOMException.ABORT_ERR only works in firefox -.- | |
507 | this.getViewModel().set('canConfirm', true); | |
508 | // FIXME: better handling, show some message, ...? | |
a8a01327 DC |
509 | me.lookup('webAuthnError').setData({ |
510 | error: Ext.htmlEncode(error.toString()), | |
511 | }); | |
512 | me.lookup('webAuthnError').setVisible(true); | |
fbeac4ea WB |
513 | return; |
514 | } finally { | |
f91481ed TL |
515 | let waitingMessage = me.lookup('webAuthnWaiting'); |
516 | if (waitingMessage) { | |
517 | waitingMessage.setVisible(false); | |
518 | } | |
fbeac4ea WB |
519 | } |
520 | ||
521 | let response = { | |
522 | id: hwrsp.id, | |
523 | type: hwrsp.type, | |
e90fdf5b | 524 | challenge: challenge.string, |
fbeac4ea WB |
525 | rawId: PBS.Utils.bytes_to_base64url(hwrsp.rawId), |
526 | response: { | |
527 | authenticatorData: PBS.Utils.bytes_to_base64url( | |
528 | hwrsp.response.authenticatorData, | |
529 | ), | |
530 | clientDataJSON: PBS.Utils.bytes_to_base64url(hwrsp.response.clientDataJSON), | |
531 | signature: PBS.Utils.bytes_to_base64url(hwrsp.response.signature), | |
532 | }, | |
533 | }; | |
534 | ||
fbeac4ea WB |
535 | await me.finishChallenge("webauthn:" + JSON.stringify(response)); |
536 | }, | |
537 | ||
538 | loginRecovery: function() { | |
539 | let me = this; | |
fbeac4ea | 540 | |
f91481ed TL |
541 | let key = me.lookup('recoveryKey').getValue(); |
542 | let _promise = me.finishChallenge(`recovery:${key}`); | |
543 | }, | |
544 | ||
545 | loginTFA: function() { | |
546 | let me = this; | |
8c8f7b5a TL |
547 | // avoid triggering more than once during challenge |
548 | me.getViewModel().set('canConfirm', false); | |
f91481ed TL |
549 | let view = me.getView(); |
550 | let tfaPanel = view.down('tabpanel').getActiveTab(); | |
551 | me[tfaPanel.handler](); | |
fbeac4ea WB |
552 | }, |
553 | ||
554 | finishChallenge: function(password) { | |
555 | let me = this; | |
556 | let view = me.getView(); | |
557 | view.cancelled = false; | |
558 | ||
559 | let params = { | |
560 | username: view.userid, | |
561 | 'tfa-challenge': view.ticket, | |
562 | password, | |
563 | }; | |
564 | ||
565 | let resolve = view.onResolve; | |
566 | let reject = view.onReject; | |
567 | view.close(); | |
568 | ||
3006d70e | 569 | return Proxmox.Async.api2({ |
fbeac4ea WB |
570 | url: '/api2/extjs/access/ticket', |
571 | method: 'POST', | |
572 | params, | |
573 | }) | |
574 | .then(resolve) | |
575 | .catch(reject); | |
576 | }, | |
577 | }, | |
578 | ||
579 | listeners: { | |
580 | close: 'onClose', | |
581 | }, | |
582 | ||
f91481ed TL |
583 | items: [{ |
584 | xtype: 'tabpanel', | |
585 | region: 'center', | |
586 | layout: 'fit', | |
587 | bodyPadding: 10, | |
f91481ed TL |
588 | items: [ |
589 | { | |
590 | xtype: 'panel', | |
591 | title: 'WebAuthn', | |
592 | iconCls: 'fa fa-fw fa-shield', | |
2dbc1a9a | 593 | confirmText: gettext('Start WebAuthn challenge'), |
f91481ed TL |
594 | handler: 'loginWebauthn', |
595 | bind: { | |
54067d82 | 596 | disabled: '{!availableChallenge.webauthn}', |
fbeac4ea | 597 | }, |
f91481ed TL |
598 | items: [ |
599 | { | |
600 | xtype: 'box', | |
44915932 | 601 | html: gettext('Please insert your authentication device and press its button'), |
f91481ed TL |
602 | }, |
603 | { | |
604 | xtype: 'box', | |
44915932 | 605 | html: gettext('Waiting for second factor.') +`<i class="fa fa-refresh fa-spin fa-fw"></i>`, |
f91481ed TL |
606 | reference: 'webAuthnWaiting', |
607 | hidden: true, | |
608 | }, | |
a8a01327 DC |
609 | { |
610 | xtype: 'box', | |
611 | data: { | |
612 | error: '', | |
613 | }, | |
614 | tpl: '<i class="fa fa-warning warning"></i> {error}', | |
615 | reference: 'webAuthnError', | |
616 | hidden: true, | |
617 | }, | |
f91481ed | 618 | ], |
fbeac4ea | 619 | }, |
f91481ed TL |
620 | { |
621 | xtype: 'panel', | |
622 | title: gettext('TOTP App'), | |
623 | iconCls: 'fa fa-fw fa-clock-o', | |
624 | handler: 'loginTotp', | |
625 | bind: { | |
54067d82 | 626 | disabled: '{!availableChallenge.totp}', |
f91481ed TL |
627 | }, |
628 | items: [ | |
629 | { | |
630 | xtype: 'textfield', | |
631 | fieldLabel: gettext('Please enter your TOTP verification code'), | |
632 | labelWidth: 300, | |
633 | name: 'totp', | |
634 | disabled: true, | |
635 | reference: 'totp', | |
636 | allowBlank: false, | |
637 | regex: /^[0-9]{6}$/, | |
a65eb0ec | 638 | regexText: gettext('TOTP codes consist of six decimal digits'), |
f91481ed TL |
639 | }, |
640 | ], | |
8ae6d28c | 641 | }, |
f91481ed TL |
642 | { |
643 | xtype: 'panel', | |
644 | title: gettext('Recovery Key'), | |
645 | iconCls: 'fa fa-fw fa-file-text-o', | |
646 | handler: 'loginRecovery', | |
647 | bind: { | |
54067d82 | 648 | disabled: '{!availableChallenge.recovery}', |
f91481ed TL |
649 | }, |
650 | items: [ | |
651 | { | |
652 | xtype: 'box', | |
653 | reference: 'availableRecovery', | |
654 | hidden: true, | |
655 | }, | |
656 | { | |
657 | xtype: 'textfield', | |
658 | fieldLabel: gettext('Please enter one of your single-use recovery keys'), | |
659 | labelWidth: 300, | |
660 | name: 'recoveryKey', | |
661 | disabled: true, | |
662 | reference: 'recoveryKey', | |
663 | allowBlank: false, | |
664 | regex: /^[0-9a-f]{4}(-[0-9a-f]{4}){3}$/, | |
a65eb0ec | 665 | regexText: gettext('Does not look like a valid recovery key'), |
f91481ed | 666 | }, |
f91481ed TL |
667 | { |
668 | xtype: 'box', | |
669 | reference: 'recoveryLow', | |
670 | hidden: true, | |
671 | html: '<i class="fa fa-exclamation-triangle warning"></i>' | |
bd768c33 | 672 | + gettext('Less than {0} recovery keys available. Please generate a new set after login!'), |
f91481ed TL |
673 | }, |
674 | ], | |
fbeac4ea | 675 | }, |
f91481ed TL |
676 | ], |
677 | }], | |
fbeac4ea WB |
678 | |
679 | buttons: [ | |
680 | { | |
f91481ed TL |
681 | handler: 'loginTFA', |
682 | reference: 'tfaButton', | |
683 | disabled: true, | |
684 | bind: { | |
2dbc1a9a | 685 | text: '{confirmText}', |
f91481ed TL |
686 | disabled: '{!canConfirm}', |
687 | }, | |
fbeac4ea WB |
688 | }, |
689 | ], | |
690 | }); |