]>
Commit | Line | Data |
---|---|---|
1 | /*global u2f*/ | |
2 | Ext.define('PVE.window.LoginWindow', { | |
3 | extend: 'Ext.window.Window', | |
4 | ||
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 | ||
20 | controller: { | |
21 | ||
22 | xclass: 'Ext.app.ViewController', | |
23 | ||
24 | onLogon: async function() { | |
25 | var me = this; | |
26 | ||
27 | var form = this.lookupReference('loginForm'); | |
28 | var unField = this.lookupReference('usernameField'); | |
29 | var saveunField = this.lookupReference('saveunField'); | |
30 | var view = this.getView(); | |
31 | ||
32 | if (!form.isValid()) { | |
33 | return; | |
34 | } | |
35 | ||
36 | let creds = form.getValues(); | |
37 | ||
38 | if (this.getViewModel().data.openid === true) { | |
39 | const redirectURL = location.origin; | |
40 | Proxmox.Utils.API2Request({ | |
41 | url: '/api2/extjs/access/openid/auth-url', | |
42 | params: { | |
43 | realm: creds.realm, | |
44 | "redirect-url": redirectURL, | |
45 | }, | |
46 | method: 'POST', | |
47 | success: function(resp, opts) { | |
48 | window.location = resp.result.data; | |
49 | }, | |
50 | failure: function(resp, opts) { | |
51 | Proxmox.Utils.authClear(); | |
52 | form.unmask(); | |
53 | Ext.MessageBox.alert( | |
54 | gettext('Error'), | |
55 | gettext('OpenID redirect failed.') + `<br>${resp.htmlStatus}`, | |
56 | ); | |
57 | }, | |
58 | }); | |
59 | return; | |
60 | } | |
61 | ||
62 | view.el.mask(gettext('Please wait...'), 'x-mask-loading'); | |
63 | ||
64 | // set or clear username | |
65 | var sp = Ext.state.Manager.getProvider(); | |
66 | if (saveunField.getValue() === true) { | |
67 | sp.set(unField.getStateId(), unField.getValue()); | |
68 | } else { | |
69 | sp.clear(unField.getStateId()); | |
70 | } | |
71 | sp.set(saveunField.getStateId(), saveunField.getValue()); | |
72 | ||
73 | try { | |
74 | // Request updated authentication mechanism: | |
75 | creds['new-format'] = 1; | |
76 | ||
77 | let resp = await Proxmox.Async.api2({ | |
78 | url: '/api2/extjs/access/ticket', | |
79 | params: creds, | |
80 | method: 'POST', | |
81 | }); | |
82 | ||
83 | let data = resp.result.data; | |
84 | if (data.ticket.startsWith("PVE:!tfa!")) { | |
85 | // Store first factor login information first: | |
86 | data.LoggedOut = true; | |
87 | Proxmox.Utils.setAuthData(data); | |
88 | ||
89 | data = await me.performTFAChallenge(data); | |
90 | ||
91 | // Fill in what we copy over from the 1st factor: | |
92 | data.CSRFPreventionToken = Proxmox.CSRFPreventionToken; | |
93 | data.username = Proxmox.UserName; | |
94 | me.success(data); | |
95 | } else if (Ext.isDefined(data.NeedTFA)) { | |
96 | // Store first factor login information first: | |
97 | data.LoggedOut = true; | |
98 | Proxmox.Utils.setAuthData(data); | |
99 | ||
100 | if (Ext.isDefined(data.U2FChallenge)) { | |
101 | me.perform_u2f(data); | |
102 | } else { | |
103 | me.perform_otp(); | |
104 | } | |
105 | } else { | |
106 | me.success(data); | |
107 | } | |
108 | } catch (error) { | |
109 | me.failure(error); | |
110 | } | |
111 | }, | |
112 | ||
113 | /* START NEW TFA CODE (pbs copy) */ | |
114 | performTFAChallenge: async function(data) { | |
115 | let me = this; | |
116 | ||
117 | let userid = data.username; | |
118 | let ticket = data.ticket; | |
119 | let challenge = JSON.parse(decodeURIComponent( | |
120 | ticket.split(':')[1].slice("!tfa!".length), | |
121 | )); | |
122 | ||
123 | let resp = await new Promise((resolve, reject) => { | |
124 | Ext.create('Proxmox.window.TfaLoginWindow', { | |
125 | userid, | |
126 | ticket, | |
127 | challenge, | |
128 | onResolve: value => resolve(value), | |
129 | onReject: reject, | |
130 | }).show(); | |
131 | }); | |
132 | ||
133 | return resp.result.data; | |
134 | }, | |
135 | /* END NEW TFA CODE (pbs copy) */ | |
136 | ||
137 | failure: function(resp) { | |
138 | var me = this; | |
139 | var view = me.getView(); | |
140 | view.el.unmask(); | |
141 | var handler = function() { | |
142 | var uf = me.lookupReference('usernameField'); | |
143 | uf.focus(true, true); | |
144 | }; | |
145 | ||
146 | let emsg = gettext("Login failed. Please try again"); | |
147 | ||
148 | if (resp.failureType === "connect") { | |
149 | emsg = gettext("Connection failure. Network error or Proxmox VE services not running?"); | |
150 | } | |
151 | ||
152 | Ext.MessageBox.alert(gettext('Error'), emsg, handler); | |
153 | }, | |
154 | success: function(data) { | |
155 | var me = this; | |
156 | var view = me.getView(); | |
157 | var handler = view.handler || Ext.emptyFn; | |
158 | handler.call(me, data); | |
159 | view.close(); | |
160 | }, | |
161 | ||
162 | perform_otp: function() { | |
163 | var me = this; | |
164 | var win = Ext.create('PVE.window.TFALoginWindow', { | |
165 | onLogin: function(value) { | |
166 | me.finish_tfa(value); | |
167 | }, | |
168 | onCancel: function() { | |
169 | Proxmox.LoggedOut = false; | |
170 | Proxmox.Utils.authClear(); | |
171 | me.getView().show(); | |
172 | }, | |
173 | }); | |
174 | win.show(); | |
175 | }, | |
176 | ||
177 | perform_u2f: function(data) { | |
178 | var me = this; | |
179 | // Show the message: | |
180 | var msg = Ext.Msg.show({ | |
181 | title: 'U2F: '+gettext('Verification'), | |
182 | message: gettext('Please press the button on your U2F Device'), | |
183 | buttons: [], | |
184 | }); | |
185 | var chlg = data.U2FChallenge; | |
186 | var key = { | |
187 | version: chlg.version, | |
188 | keyHandle: chlg.keyHandle, | |
189 | }; | |
190 | u2f.sign(chlg.appId, chlg.challenge, [key], function(res) { | |
191 | msg.close(); | |
192 | if (res.errorCode) { | |
193 | Proxmox.Utils.authClear(); | |
194 | Ext.Msg.alert(gettext('Error'), PVE.Utils.render_u2f_error(res.errorCode)); | |
195 | return; | |
196 | } | |
197 | delete res.errorCode; | |
198 | me.finish_tfa(JSON.stringify(res)); | |
199 | }); | |
200 | }, | |
201 | finish_tfa: function(res) { | |
202 | var me = this; | |
203 | var view = me.getView(); | |
204 | view.el.mask(gettext('Please wait...'), 'x-mask-loading'); | |
205 | Proxmox.Utils.API2Request({ | |
206 | url: '/api2/extjs/access/tfa', | |
207 | params: { | |
208 | response: res, | |
209 | }, | |
210 | method: 'POST', | |
211 | timeout: 5000, // it'll delay both success & failure | |
212 | success: function(resp, opts) { | |
213 | view.el.unmask(); | |
214 | // Fill in what we copy over from the 1st factor: | |
215 | var data = resp.result.data; | |
216 | data.CSRFPreventionToken = Proxmox.CSRFPreventionToken; | |
217 | data.username = Proxmox.UserName; | |
218 | // Finish logging in: | |
219 | me.success(data); | |
220 | }, | |
221 | failure: function(resp, opts) { | |
222 | Proxmox.Utils.authClear(); | |
223 | me.failure(resp); | |
224 | }, | |
225 | }); | |
226 | }, | |
227 | ||
228 | control: { | |
229 | 'field[name=username]': { | |
230 | specialkey: function(f, e) { | |
231 | if (e.getKey() === e.ENTER) { | |
232 | var pf = this.lookupReference('passwordField'); | |
233 | if (!pf.getValue()) { | |
234 | pf.focus(false); | |
235 | } | |
236 | } | |
237 | }, | |
238 | }, | |
239 | 'field[name=lang]': { | |
240 | change: function(f, value) { | |
241 | var dt = Ext.Date.add(new Date(), Ext.Date.YEAR, 10); | |
242 | Ext.util.Cookies.set('PVELangCookie', value, dt); | |
243 | this.getView().mask(gettext('Please wait...'), 'x-mask-loading'); | |
244 | window.location.reload(); | |
245 | }, | |
246 | }, | |
247 | 'field[name=realm]': { | |
248 | change: function(f, value) { | |
249 | let record = f.store.getById(value); | |
250 | if (record === undefined) return; | |
251 | let data = record.data; | |
252 | this.getViewModel().set("openid", data.type === "openid"); | |
253 | }, | |
254 | }, | |
255 | 'button[reference=loginButton]': { | |
256 | click: 'onLogon', | |
257 | }, | |
258 | '#': { | |
259 | show: function() { | |
260 | var me = this; | |
261 | ||
262 | var sp = Ext.state.Manager.getProvider(); | |
263 | var checkboxField = this.lookupReference('saveunField'); | |
264 | var unField = this.lookupReference('usernameField'); | |
265 | ||
266 | var checked = sp.get(checkboxField.getStateId()); | |
267 | checkboxField.setValue(checked); | |
268 | ||
269 | if (checked === true) { | |
270 | var username = sp.get(unField.getStateId()); | |
271 | unField.setValue(username); | |
272 | var pwField = this.lookupReference('passwordField'); | |
273 | pwField.focus(); | |
274 | } | |
275 | ||
276 | let auth = Proxmox.Utils.getOpenIDRedirectionAuthorization(); | |
277 | if (auth !== undefined) { | |
278 | Proxmox.Utils.authClear(); | |
279 | ||
280 | let loginForm = this.lookupReference('loginForm'); | |
281 | loginForm.mask(gettext('OpenID login - please wait...'), 'x-mask-loading'); | |
282 | ||
283 | const redirectURL = location.origin; | |
284 | ||
285 | Proxmox.Utils.API2Request({ | |
286 | url: '/api2/extjs/access/openid/login', | |
287 | params: { | |
288 | state: auth.state, | |
289 | code: auth.code, | |
290 | "redirect-url": redirectURL, | |
291 | }, | |
292 | method: 'POST', | |
293 | failure: function(response) { | |
294 | loginForm.unmask(); | |
295 | let error = response.htmlStatus; | |
296 | Ext.MessageBox.alert( | |
297 | gettext('Error'), | |
298 | gettext('OpenID login failed, please try again') + `<br>${error}`, | |
299 | () => { window.location = redirectURL; }, | |
300 | ); | |
301 | }, | |
302 | success: function(response, options) { | |
303 | loginForm.unmask(); | |
304 | let data = response.result.data; | |
305 | history.replaceState(null, '', redirectURL); | |
306 | me.success(data); | |
307 | }, | |
308 | }); | |
309 | } | |
310 | }, | |
311 | }, | |
312 | }, | |
313 | }, | |
314 | ||
315 | width: 400, | |
316 | modal: true, | |
317 | border: false, | |
318 | draggable: true, | |
319 | closable: false, | |
320 | resizable: false, | |
321 | layout: 'auto', | |
322 | ||
323 | title: gettext('Proxmox VE Login'), | |
324 | ||
325 | defaultFocus: 'usernameField', | |
326 | defaultButton: 'loginButton', | |
327 | ||
328 | items: [{ | |
329 | xtype: 'form', | |
330 | layout: 'form', | |
331 | url: '/api2/extjs/access/ticket', | |
332 | reference: 'loginForm', | |
333 | ||
334 | fieldDefaults: { | |
335 | labelAlign: 'right', | |
336 | allowBlank: false, | |
337 | }, | |
338 | ||
339 | items: [ | |
340 | { | |
341 | xtype: 'textfield', | |
342 | fieldLabel: gettext('User name'), | |
343 | name: 'username', | |
344 | itemId: 'usernameField', | |
345 | reference: 'usernameField', | |
346 | stateId: 'login-username', | |
347 | bind: { | |
348 | visible: "{!openid}", | |
349 | disabled: "{openid}", | |
350 | }, | |
351 | }, | |
352 | { | |
353 | xtype: 'textfield', | |
354 | inputType: 'password', | |
355 | fieldLabel: gettext('Password'), | |
356 | name: 'password', | |
357 | reference: 'passwordField', | |
358 | bind: { | |
359 | visible: "{!openid}", | |
360 | disabled: "{openid}", | |
361 | }, | |
362 | }, | |
363 | { | |
364 | xtype: 'pmxRealmComboBox', | |
365 | name: 'realm', | |
366 | }, | |
367 | { | |
368 | xtype: 'proxmoxLanguageSelector', | |
369 | fieldLabel: gettext('Language'), | |
370 | value: Ext.util.Cookies.get('PVELangCookie') || Proxmox.defaultLang || 'en', | |
371 | name: 'lang', | |
372 | reference: 'langField', | |
373 | submitValue: false, | |
374 | }, | |
375 | ], | |
376 | buttons: [ | |
377 | { | |
378 | xtype: 'checkbox', | |
379 | fieldLabel: gettext('Save User name'), | |
380 | name: 'saveusername', | |
381 | reference: 'saveunField', | |
382 | stateId: 'login-saveusername', | |
383 | labelWidth: 250, | |
384 | labelAlign: 'right', | |
385 | submitValue: false, | |
386 | bind: { | |
387 | visible: "{!openid}", | |
388 | }, | |
389 | }, | |
390 | { | |
391 | bind: { | |
392 | text: "{button_text}", | |
393 | }, | |
394 | reference: 'loginButton', | |
395 | }, | |
396 | ], | |
397 | }], | |
398 | }); |