]> git.proxmox.com Git - pve-manager.git/blame - www/manager6/storage/PBSEdit.js
ui: qemu: eslint fixes
[pve-manager.git] / www / manager6 / storage / PBSEdit.js
CommitLineData
d1a7c6ee 1/*global QRCode*/
d1a7c6ee
TL
2Ext.define('PVE.Storage.PBSKeyShow', {
3 extend: 'Ext.window.Window',
cfc3a166 4 xtype: 'pvePBSKeyShow',
d1a7c6ee
TL
5 mixins: ['Proxmox.Mixin.CBind'],
6
7 width: 600,
8 modal: true,
9 resizable: false,
10 title: gettext('Important: Save your Encryption Key'),
11
12 // avoid that esc closes this by mistake, force user to more manual action
13 onEsc: Ext.emptyFn,
14 closable: false,
15
16 items: [
17 {
18 xtype: 'form',
19 layout: {
20 type: 'vbox',
21 align: 'stretch',
22 },
23 bodyPadding: 10,
24 border: false,
25 defaults: {
26 anchor: '100%',
27 border: false,
28 padding: '10 0 0 0',
29 },
30 items: [
31 {
32 xtype: 'textfield',
33 fieldLabel: gettext('Key'),
13a04340 34 labelWidth: 80,
d1a7c6ee
TL
35 inputId: 'encryption-key-value',
36 cbind: {
37 value: '{key}',
38 },
39 editable: false,
40 },
41 {
42 xtype: 'component',
a6f6ed5a 43 html: gettext('Keep your encryption key safe, but easily accessible for disaster recovery.')
d1a7c6ee
TL
44 + '<br>' + gettext('We recommend the following safe-keeping strategy:'),
45 },
46 {
47 xtyp: 'container',
48 layout: 'hbox',
49 items: [
50 {
51 xtype: 'component',
52 html: '1. ' + gettext('Save the key in your password manager.'),
53 flex: 1,
54 },
55 {
56 xtype: 'button',
57 text: gettext('Copy Key'),
58 iconCls: 'fa fa-clipboard x-btn-icon-el-default-toolbar-small',
59 cls: 'x-btn-default-toolbar-small proxmox-inline-button',
60 width: 110,
61 handler: function(b) {
62 document.getElementById('encryption-key-value').select();
63 document.execCommand("copy");
64 },
65 },
66 ],
67 },
68 {
69 xtype: 'container',
70 layout: 'hbox',
71 items: [
72 {
73 xtype: 'component',
74 html: '2. ' + gettext('Download the key to a USB (pen) drive, placed in secure vault.'),
75 flex: 1,
76 },
77 {
78 xtype: 'button',
79 text: gettext('Download'),
80 iconCls: 'fa fa-download x-btn-icon-el-default-toolbar-small',
81 cls: 'x-btn-default-toolbar-small proxmox-inline-button',
82 width: 110,
83 handler: function(b) {
84 let win = this.up('window');
85
86 let pveID = PVE.ClusterName || window.location.hostname;
87 let name = `pve-${pveID}-storage-${win.sid}.enc`;
88
89 let hiddenElement = document.createElement('a');
90 hiddenElement.href = 'data:attachment/text,' + encodeURI(win.key);
91 hiddenElement.target = '_blank';
92 hiddenElement.download = name;
93 hiddenElement.click();
94 },
95 },
96 ],
97 },
98 {
99 xtype: 'container',
100 layout: 'hbox',
101 items: [
102 {
103 xtype: 'component',
104 html: '3. ' + gettext('Print as paperkey, laminated and placed in secure vault.'),
105 flex: 1,
106 },
107 {
108 xtype: 'button',
109 text: gettext('Print Key'),
110 iconCls: 'fa fa-print x-btn-icon-el-default-toolbar-small',
111 cls: 'x-btn-default-toolbar-small proxmox-inline-button',
112 width: 110,
113 handler: function(b) {
114 let win = this.up('window');
115 win.paperkey(win.key);
116 },
117 },
118 ],
119 },
120 ],
121 },
122 {
123 xtype: 'component',
124 border: false,
125 padding: '10 10 10 10',
126 userCls: 'pmx-hint',
ba854589 127 html: gettext('Please save the encryption key - losing it will render any backup created with it unusable'),
d1a7c6ee
TL
128 },
129 ],
130 buttons: [
131 {
132 text: gettext('Close'),
133 handler: function(b) {
134 let win = this.up('window');
135 win.close();
136 },
137 },
138 ],
cfc3a166 139 paperkey: function(keyString) {
d1a7c6ee
TL
140 let me = this;
141
cfc3a166
TL
142 const key = JSON.parse(keyString);
143
d1a7c6ee
TL
144 const qrwidth = 500;
145 let qrdiv = document.createElement('div');
146 let qrcode = new QRCode(qrdiv, {
147 width: qrwidth,
148 height: qrwidth,
149 correctLevel: QRCode.CorrectLevel.H,
150 });
cfc3a166
TL
151 qrcode.makeCode(keyString);
152
153 let shortKeyFP = '';
154 if (key.fingerprint) {
155 shortKeyFP = PVE.Utils.render_pbs_fingerprint(key.fingerprint);
156 }
d1a7c6ee
TL
157
158 let printFrame = document.createElement("iframe");
159 Object.assign(printFrame.style, {
160 position: "fixed",
161 right: "0",
162 bottom: "0",
163 width: "0",
164 height: "0",
165 border: "0",
166 });
cfc3a166 167 const prettifiedKey = JSON.stringify(key, null, 2);
d1a7c6ee
TL
168 const keyQrBase64 = qrdiv.children[0].toDataURL("image/png");
169 const html = `<html><head><script>
170 window.addEventListener('DOMContentLoaded', (ev) => window.print());
171 </script><style>@media print and (max-height: 150mm) {
172 h4, p { margin: 0; font-size: 1em; }
173 }</style></head><body style="padding: 5px;">
cfc3a166
TL
174 <h4>Encryption Key - Storage '${me.sid}' (${shortKeyFP})</h4>
175<p style="font-size:1.2em;font-family:monospace;white-space:pre-wrap;overflow-wrap:break-word;">
d1a7c6ee
TL
176-----BEGIN PROXMOX BACKUP KEY-----
177${prettifiedKey}
178-----END PROXMOX BACKUP KEY-----</p>
179 <center><img style="width: 100%; max-width: ${qrwidth}px;" src="${keyQrBase64}"></center>
180 </body></html>`;
181
182 printFrame.src = "data:text/html;base64," + btoa(html);
183 document.body.appendChild(printFrame);
184 },
185});
186
14ba33fb
TL
187Ext.define('PVE.panel.PBSEncryptionKeyTab', {
188 extend: 'Proxmox.panel.InputPanel',
189 xtype: 'pvePBSEncryptionKeyTab',
190 mixins: ['Proxmox.Mixin.CBind'],
191
903e646d
TL
192 onlineHelp: 'storage_pbs_encryption',
193
14ba33fb
TL
194 onGetValues: function(form) {
195 let values = {};
196 if (form.cryptMode === 'upload') {
197 values['encryption-key'] = form['crypt-key-upload'];
198 } else if (form.cryptMode === 'autogenerate') {
199 values['encryption-key'] = 'autogen';
200 } else if (form.cryptMode === 'none') {
201 if (!this.isCreate) {
202 values.delete = ['encryption-key'];
203 }
204 }
205 return values;
206 },
207
208 setValues: function(values) {
209 let me = this;
210 let vm = me.getViewModel();
211
212 let cryptKeyInfo = values['encryption-key'];
213 if (cryptKeyInfo) {
8058410f 214 let icon = '<span class="fa fa-lock good"></span> ';
14ba33fb
TL
215 if (cryptKeyInfo.match(/^[a-fA-F0-9]{2}:/)) { // new style fingerprint
216 let shortKeyFP = PVE.Utils.render_pbs_fingerprint(cryptKeyInfo);
217 values['crypt-key-fp'] = icon + `${gettext('Active')} - ${gettext('Fingerprint')} ${shortKeyFP}`;
218 } else {
219 // old key without FP
220 values['crypt-key-fp'] = icon + gettext('Active');
221 }
222 } else {
223 values['crypt-key-fp'] = gettext('None');
224 let cryptModeNone = me.down('radiofield[inputValue=none]');
225 cryptModeNone.setBoxLabel(gettext('Do not encrypt backups'));
226 cryptModeNone.setValue(true);
227 }
228 vm.set('keepCryptVisible', !!cryptKeyInfo);
229 vm.set('allowEdit', !cryptKeyInfo);
230
231 me.callParent([values]);
232 },
233
234 viewModel: {
235 data: {
236 allowEdit: true,
237 keepCryptVisible: false,
238 },
239 formulas: {
240 showDangerousHint: get => {
241 let allowEdit = get('allowEdit');
242 return get('keepCryptVisible') && allowEdit;
243 },
244 },
245 },
246
247 items: [
248 {
249 xtype: 'displayfield',
250 name: 'crypt-key-fp',
251 fieldLabel: gettext('Encryption Key'),
252 padding: '2 0',
253 },
254 {
255 xtype: 'checkbox',
256 name: 'crypt-allow-edit',
257 boxLabel: gettext('Edit existing encryption key (dangerous!)'),
258 hidden: true,
259 submitValue: false,
260 isDirty: () => false,
261 bind: {
262 hidden: '{!keepCryptVisible}',
263 value: '{allowEdit}',
264 },
265 },
266 {
267 xtype: 'radiofield',
268 name: 'cryptMode',
269 inputValue: 'keep',
270 boxLabel: gettext('Keep encryption key'),
271 padding: '0 0 0 25',
272 cbind: {
273 hidden: '{isCreate}',
274 checked: '{!isCreate}',
275 },
276 bind: {
277 hidden: '{!keepCryptVisible}',
278 disabled: '{!allowEdit}',
279 },
280 },
281 {
282 xtype: 'radiofield',
283 name: 'cryptMode',
284 inputValue: 'none',
285 checked: true,
286 padding: '0 0 0 25',
287 cbind: {
288 disabled: '{!isCreate}',
289 checked: '{isCreate}',
290 boxLabel: get => get('isCreate')
291 ? gettext('Do not encrypt backups')
292 : gettext('Delete existing encryption key'),
293 },
294 bind: {
295 disabled: '{!allowEdit}',
296 },
297 },
298 {
299 xtype: 'radiofield',
300 name: 'cryptMode',
301 inputValue: 'autogenerate',
302 boxLabel: gettext('Auto-generate a client encryption key'),
303 padding: '0 0 0 25',
304 cbind: {
305 disabled: '{!isCreate}',
306 },
307 bind: {
308 disabled: '{!allowEdit}',
309 },
310 },
311 {
312 xtype: 'radiofield',
313 name: 'cryptMode',
314 inputValue: 'upload',
315 boxLabel: gettext('Upload an existing client encryption key'),
316 padding: '0 0 0 25',
317 cbind: {
318 disabled: '{!isCreate}',
319 },
320 bind: {
321 disabled: '{!allowEdit}',
322 },
323 listeners: {
324 change: function(f, value) {
325 let panel = this.up('inputpanel');
326 if (!panel.rendered) {
327 return;
328 }
329 let uploadKeyField = panel.down('field[name=crypt-key-upload]');
330 uploadKeyField.setDisabled(!value);
331 uploadKeyField.setHidden(!value);
332
333 let uploadKeyButton = panel.down('filebutton[name=crypt-upload-button]');
334 uploadKeyButton.setDisabled(!value);
335 uploadKeyButton.setHidden(!value);
336
337 if (value) {
338 uploadKeyField.validate();
339 } else {
340 uploadKeyField.reset();
341 }
342 },
343 },
344 },
345 {
346 xtype: 'fieldcontainer',
347 layout: 'hbox',
348 items: [
349 {
350 xtype: 'proxmoxtextfield',
351 name: 'crypt-key-upload',
352 fieldLabel: gettext('Key'),
353 value: '',
354 disabled: true,
355 hidden: true,
356 allowBlank: false,
357 labelAlign: 'right',
358 flex: 1,
359 emptyText: gettext('You can drag-and-drop a key file here.'),
360 validator: function(value) {
361 if (value.length) {
362 let key;
363 try {
364 key = JSON.parse(value);
365 } catch (e) {
366 return "Failed to parse key - " + e;
367 }
368 if (typeof key.data === undefined) {
369 return "Does not seems like a valid Proxmox Backup key!";
370 }
371 }
372 return true;
373 },
374 afterRender: function() {
375 let field = this;
376 if (!window.FileReader) {
377 // No FileReader support in this browser
378 return;
379 }
380 let cancel = function(ev) {
381 ev = ev.event;
382 if (ev.preventDefault) {
383 ev.preventDefault();
384 }
385 };
386 field.inputEl.on('dragover', cancel);
387 field.inputEl.on('dragenter', cancel);
388 field.inputEl.on('drop', function(ev) {
389 ev = ev.event;
390 if (ev.preventDefault) {
391 ev.preventDefault();
392 }
393 let files = ev.dataTransfer.files;
394 PVE.Utils.loadTextFromFile(files[0], v => field.setValue(v));
395 });
396 },
397 },
398 {
399 xtype: 'filebutton',
400 name: 'crypt-upload-button',
401 iconCls: 'fa fa-fw fa-folder-open-o x-btn-icon-el-default-toolbar-small',
402 cls: 'x-btn-default-toolbar-small proxmox-inline-button',
403 margin: '0 0 0 4',
404 disabled: true,
405 hidden: true,
406 listeners: {
407 change: function(btn, e, value) {
408 let ev = e.event;
409 let field = btn.up().down('proxmoxtextfield[name=crypt-key-upload]');
410 PVE.Utils.loadTextFromFile(ev.target.files[0], v => field.setValue(v));
411 btn.reset();
412 },
413 },
414 },
415 ],
416 },
417 {
418 xtype: 'component',
419 border: false,
420 padding: '5 2',
421 userCls: 'pmx-hint',
422 html: // `<b style="color:red;font-weight:600;">${gettext('Warning')}</b>: ` +
423 `<span class="fa fa-exclamation-triangle" style="color:red;font-size:14px;"></span> ` +
424 gettext('Deleting or replacing the encryption key will break restoring backups created with it!'),
425 hidden: true,
426 bind: {
427 hidden: '{!showDangerousHint}',
428 },
429 },
430 ],
431});
432
ee19d331
TL
433Ext.define('PVE.storage.PBSInputPanel', {
434 extend: 'PVE.panel.StorageBase',
435
903e646d 436 onlineHelp: 'storage_pbs',
ee19d331 437
d1a7c6ee
TL
438 apiCallDone: function(success, response, options) {
439 let res = response.result.data;
440 if (!(res && res.config && res.config['encryption-key'])) {
441 return;
442 }
443 let key = res.config['encryption-key'];
444 Ext.create('PVE.Storage.PBSKeyShow', {
445 autoShow: true,
446 sid: res.storage,
447 key: key,
448 });
449 },
450
5b7ab402
TL
451 isPBS: true, // HACK
452
14ba33fb
TL
453 extraTabs: [
454 {
455 xtype: 'pvePBSEncryptionKeyTab',
456 title: gettext('Encryption'),
457 },
458 ],
459
ee19d331
TL
460 initComponent: function() {
461 var me = this;
462
463 me.column1 = [
464 {
465 xtype: me.isCreate ? 'textfield' : 'displayfield',
466 name: 'server',
467 value: '',
468 vtype: 'DnsOrIp',
469 fieldLabel: gettext('Server'),
470 allowBlank: false,
471 },
472 {
473 xtype: me.isCreate ? 'textfield' : 'displayfield',
474 name: 'username',
475 value: '',
476 emptyText: gettext('Example') + ': admin@pbs',
477 fieldLabel: gettext('Username'),
478 regex: /\S+@\w+/,
479 regexText: gettext('Example') + ': admin@pbs',
480 allowBlank: false,
481 },
482 {
483 xtype: me.isCreate ? 'textfield' : 'displayfield',
484 inputType: 'password',
485 name: 'password',
486 value: me.isCreate ? '' : '********',
487 emptyText: me.isCreate ? gettext('None') : '',
488 fieldLabel: gettext('Password'),
303dc33f 489 allowBlank: false,
ee19d331 490 },
ee19d331
TL
491 ];
492
493 me.column2 = [
ee19d331
TL
494 {
495 xtype: 'displayfield',
496 name: 'content',
497 value: 'backup',
498 submitValue: true,
499 fieldLabel: gettext('Content'),
500 },
db364c61
TL
501 {
502 xtype: me.isCreate ? 'textfield' : 'displayfield',
503 name: 'datastore',
504 value: '',
505 fieldLabel: 'Datastore',
506 allowBlank: false,
507 },
ee19d331
TL
508 ];
509
510 me.columnB = [
511 {
76fa1b43 512 xtype: 'proxmoxtextfield',
ee19d331 513 name: 'fingerprint',
76fa1b43 514 value: me.isCreate ? null : undefined,
ee19d331 515 fieldLabel: gettext('Fingerprint'),
d3d1e199 516 emptyText: gettext('Server certificate SHA-256 fingerprint, required for self-signed certificates'),
ee19d331
TL
517 regex: /[A-Fa-f0-9]{2}(:[A-Fa-f0-9]{2}){31}/,
518 regexText: gettext('Example') + ': AB:CD:EF:...',
519 allowBlank: true,
520 },
521 ];
522
523 me.callParent();
524 },
525});