]> git.proxmox.com Git - proxmox-widget-toolkit.git/blame - src/window/AddTotp.js
language selector: increase only picker list view
[proxmox-widget-toolkit.git] / src / window / AddTotp.js
CommitLineData
64176447
WB
1/*global QRCode*/
2Ext.define('Proxmox.window.AddTotp', {
3 extend: 'Proxmox.window.Edit',
4 alias: 'widget.pmxAddTotp',
5 mixins: ['Proxmox.Mixin.CBind'],
6
7 onlineHelp: 'user_mgmt',
8
9 modal: true,
10 resizable: false,
11 title: gettext('Add a TOTP login factor'),
12 width: 512,
13 layout: {
14 type: 'vbox',
15 align: 'stretch',
16 },
17
18 isAdd: true,
19 userid: undefined,
20 tfa_id: undefined,
d6f0eee9 21 issuerName: `Proxmox - ${document?.location?.hostname || 'unknown'}`,
64176447
WB
22 fixedUser: false,
23
24 updateQrCode: function() {
25 let me = this;
26 let values = me.lookup('totp_form').getValues();
27 let algorithm = values.algorithm;
28 if (!algorithm) {
29 algorithm = 'SHA1';
30 }
31
32 let otpuri =
33 'otpauth://totp/' +
34 encodeURIComponent(values.issuer) +
35 ':' +
36 encodeURIComponent(values.userid) +
37 '?secret=' + values.secret +
38 '&period=' + values.step +
39 '&digits=' + values.digits +
40 '&algorithm=' + algorithm +
41 '&issuer=' + encodeURIComponent(values.issuer);
42
43 me.getController().getViewModel().set('otpuri', otpuri);
44 me.qrcode.makeCode(otpuri);
45 me.lookup('challenge').setVisible(true);
46 me.down('#qrbox').setVisible(true);
47 },
48
49 viewModel: {
50 data: {
51 valid: false,
52 secret: '',
53 otpuri: '',
54 userid: null,
55 },
56
57 formulas: {
58 secretEmpty: function(get) {
59 return get('secret').length === 0;
60 },
61 },
62 },
63
64 controller: {
65 xclass: 'Ext.app.ViewController',
66 control: {
67 'field[qrupdate=true]': {
68 change: function() {
69 this.getView().updateQrCode();
70 },
71 },
72 'field': {
73 validitychange: function(field, valid) {
74 let me = this;
75 let viewModel = me.getViewModel();
76 let form = me.lookup('totp_form');
77 let challenge = me.lookup('challenge');
78 let password = me.lookup('password');
79 viewModel.set('valid', form.isValid() && challenge.isValid() && password.isValid());
80 },
81 },
82 '#': {
83 show: function() {
84 let me = this;
85 let view = me.getView();
86
87 view.qrdiv = document.createElement('div');
88 view.qrcode = new QRCode(view.qrdiv, {
89 width: 256,
90 height: 256,
91 correctLevel: QRCode.CorrectLevel.M,
92 });
93 view.down('#qrbox').getEl().appendChild(view.qrdiv);
94
95 view.getController().randomizeSecret();
96 },
97 },
98 },
99
100 randomizeSecret: function() {
101 let me = this;
102 let rnd = new Uint8Array(32);
103 window.crypto.getRandomValues(rnd);
104 let data = '';
105 rnd.forEach(function(b) {
106 // secret must be base32, so just use the first 5 bits
107 b = b & 0x1f;
108 if (b < 26) {
109 // A..Z
110 data += String.fromCharCode(b + 0x41);
111 } else {
112 // 2..7
113 data += String.fromCharCode(b-26 + 0x32);
114 }
115 });
116 me.getViewModel().set('secret', data);
117 },
118 },
119
120 items: [
121 {
122 xtype: 'form',
123 layout: 'anchor',
124 border: false,
125 reference: 'totp_form',
126 fieldDefaults: {
127 anchor: '100%',
128 },
129 items: [
130 {
131 xtype: 'pmxDisplayEditField',
132 name: 'userid',
133 cbind: {
134 editable: (get) => get('isAdd') && !get('fixedUser'),
135 value: () => Proxmox.UserName,
136 },
137 fieldLabel: gettext('User'),
138 editConfig: {
139 xtype: 'pmxUserSelector',
140 allowBlank: false,
141 },
142 renderer: Ext.String.htmlEncode,
143 listeners: {
144 change: function(field, newValue, oldValue) {
145 let vm = this.up('window').getViewModel();
146 vm.set('userid', newValue);
147 },
148 },
149 qrupdate: true,
150 },
151 {
152 xtype: 'textfield',
153 fieldLabel: gettext('Description'),
154 emptyText: gettext('For example: TFA device ID, required to identify multiple factors.'),
155 allowBlank: false,
156 name: 'description',
157 maxLength: 256,
158 },
159 {
160 layout: 'hbox',
161 border: false,
162 padding: '0 0 5 0',
163 items: [
164 {
165 xtype: 'textfield',
166 fieldLabel: gettext('Secret'),
167 emptyText: gettext('Unchanged'),
168 name: 'secret',
169 reference: 'tfa_secret',
170 regex: /^[A-Z2-7=]+$/,
171 regexText: 'Must be base32 [A-Z2-7=]',
172 maskRe: /[A-Z2-7=]/,
173 qrupdate: true,
174 bind: {
175 value: "{secret}",
176 },
177 flex: 4,
178 padding: '0 5 0 0',
179 },
180 {
181 xtype: 'button',
182 text: gettext('Randomize'),
183 reference: 'randomize_button',
184 handler: 'randomizeSecret',
185 flex: 1,
186 },
187 ],
188 },
189 {
190 xtype: 'numberfield',
191 fieldLabel: gettext('Time period'),
192 name: 'step',
193 // Google Authenticator ignores this and generates bogus data
194 hidden: true,
195 value: 30,
196 minValue: 10,
197 qrupdate: true,
198 },
199 {
200 xtype: 'numberfield',
201 fieldLabel: gettext('Digits'),
202 name: 'digits',
203 value: 6,
204 // Google Authenticator ignores this and generates bogus data
205 hidden: true,
206 minValue: 6,
207 maxValue: 8,
208 qrupdate: true,
209 },
210 {
211 xtype: 'textfield',
212 fieldLabel: gettext('Issuer Name'),
213 name: 'issuer',
214 cbind: {
215 value: '{issuerName}',
216 },
217 qrupdate: true,
218 },
219 {
220 xtype: 'box',
221 itemId: 'qrbox',
222 visible: false, // will be enabled when generating a qr code
223 bind: {
224 visible: '{!secretEmpty}',
225 },
226 style: {
745aeaac 227 margin: '5px auto',
64176447
WB
228 padding: '5px',
229 width: '266px',
230 height: '266px',
745aeaac 231 'background-color': 'white',
64176447
WB
232 },
233 },
234 {
235 xtype: 'textfield',
236 fieldLabel: gettext('Verify Code'),
237 allowBlank: false,
238 reference: 'challenge',
239 name: 'challenge',
240 bind: {
241 disabled: '{!showTOTPVerifiction}',
242 visible: '{showTOTPVerifiction}',
243 },
244 emptyText: gettext('Scan QR code in a TOTP app and enter an auth. code here'),
245 },
246 {
247 xtype: 'textfield',
248 name: 'password',
249 reference: 'password',
250 fieldLabel: gettext('Verify Password'),
251 inputType: 'password',
252 minLength: 5,
253 allowBlank: false,
254 validateBlank: true,
255 cbind: {
256 hidden: () => Proxmox.UserName === 'root@pam',
257 disabled: () => Proxmox.UserName === 'root@pam',
258 emptyText: () =>
259 Ext.String.format(gettext("Confirm your ({0}) password"), Proxmox.UserName),
260 },
261 },
262 ],
263 },
264 ],
265
266 initComponent: function() {
267 let me = this;
268 me.url = '/api2/extjs/access/tfa/';
269 me.method = 'POST';
270 me.callParent();
271 },
272
273 getValues: function(dirtyOnly) {
274 let me = this;
275 let viewmodel = me.getController().getViewModel();
276
277 let values = me.callParent(arguments);
278
279 let uid = encodeURIComponent(values.userid);
280 me.url = `/api2/extjs/access/tfa/${uid}`;
281 delete values.userid;
282
283 let data = {
284 description: values.description,
285 type: "totp",
286 totp: viewmodel.get('otpuri'),
287 value: values.challenge,
288 };
289
290 if (values.password) {
291 data.password = values.password;
292 }
293
294 return data;
295 },
296});