]> git.proxmox.com Git - pve-manager.git/blobdiff - www/manager6/storage/PBSEdit.js
ui: eslint: fix various spacing related issues
[pve-manager.git] / www / manager6 / storage / PBSEdit.js
index b366400440cc533fc688ad9b9fd3a2caa5fe4538..2479304a277931cc08a1731e49a1c81f7e4d125b 100644 (file)
@@ -1,7 +1,461 @@
+/*global QRCode*/
+Ext.define('PVE.Storage.PBSKeyShow', {
+    extend: 'Ext.window.Window',
+    xtype: 'pvePBSKeyShow',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    width: 600,
+    modal: true,
+    resizable: false,
+    title: gettext('Important: Save your Encryption Key'),
+
+    // avoid that esc closes this by mistake, force user to more manual action
+    onEsc: Ext.emptyFn,
+    closable: false,
+
+    items: [
+       {
+           xtype: 'form',
+           layout: {
+               type: 'vbox',
+               align: 'stretch',
+           },
+           bodyPadding: 10,
+           border: false,
+           defaults: {
+               anchor: '100%',
+               border: false,
+               padding: '10 0 0 0',
+            },
+           items: [
+               {
+                   xtype: 'textfield',
+                   fieldLabel: gettext('Key'),
+                   labelWidth: 80,
+                   inputId: 'encryption-key-value',
+                   cbind: {
+                       value: '{key}',
+                   },
+                   editable: false,
+               },
+               {
+                   xtype: 'component',
+                   html: gettext('Keep your encryption key safe, but easily accessible for disaster recovery.')
+                       + '<br>' + gettext('We recommend the following safe-keeping strategy:'),
+               },
+               {
+                   xtyp: 'container',
+                   layout: 'hbox',
+                   items: [
+                       {
+                           xtype: 'component',
+                           html: '1. ' + gettext('Save the key in your password manager.'),
+                           flex: 1,
+                       },
+                       {
+                           xtype: 'button',
+                           text: gettext('Copy Key'),
+                           iconCls: 'fa fa-clipboard x-btn-icon-el-default-toolbar-small',
+                           cls: 'x-btn-default-toolbar-small proxmox-inline-button',
+                           width: 110,
+                           handler: function(b) {
+                               document.getElementById('encryption-key-value').select();
+                               document.execCommand("copy");
+                           },
+                       },
+                   ],
+               },
+               {
+                   xtype: 'container',
+                   layout: 'hbox',
+                   items: [
+                       {
+                           xtype: 'component',
+                           html: '2. ' + gettext('Download the key to a USB (pen) drive, placed in secure vault.'),
+                           flex: 1,
+                       },
+                       {
+                           xtype: 'button',
+                           text: gettext('Download'),
+                           iconCls: 'fa fa-download x-btn-icon-el-default-toolbar-small',
+                           cls: 'x-btn-default-toolbar-small proxmox-inline-button',
+                           width: 110,
+                           handler: function(b) {
+                               let win = this.up('window');
+
+                               let pveID = PVE.ClusterName || window.location.hostname;
+                               let name = `pve-${pveID}-storage-${win.sid}.enc`;
+
+                               let hiddenElement = document.createElement('a');
+                               hiddenElement.href = 'data:attachment/text,' + encodeURI(win.key);
+                               hiddenElement.target = '_blank';
+                               hiddenElement.download = name;
+                               hiddenElement.click();
+                           },
+                       },
+                   ],
+               },
+               {
+                   xtype: 'container',
+                   layout: 'hbox',
+                   items: [
+                       {
+                           xtype: 'component',
+                           html: '3. ' + gettext('Print as paperkey, laminated and placed in secure vault.'),
+                           flex: 1,
+                       },
+                       {
+                           xtype: 'button',
+                           text: gettext('Print Key'),
+                           iconCls: 'fa fa-print x-btn-icon-el-default-toolbar-small',
+                           cls: 'x-btn-default-toolbar-small proxmox-inline-button',
+                           width: 110,
+                           handler: function(b) {
+                               let win = this.up('window');
+                               win.paperkey(win.key);
+                           },
+                       },
+                   ],
+               },
+           ],
+       },
+       {
+           xtype: 'component',
+           border: false,
+           padding: '10 10 10 10',
+           userCls: 'pmx-hint',
+           html: gettext('Please save the encryption key - losing it will render any backup created with it unusable'),
+       },
+    ],
+    buttons: [
+       {
+           text: gettext('Close'),
+           handler: function(b) {
+               let win = this.up('window');
+               win.close();
+           },
+       },
+    ],
+    paperkey: function(keyString) {
+       let me = this;
+
+       const key = JSON.parse(keyString);
+
+       const qrwidth = 500;
+       let qrdiv = document.createElement('div');
+       let qrcode = new QRCode(qrdiv, {
+           width: qrwidth,
+           height: qrwidth,
+           correctLevel: QRCode.CorrectLevel.H,
+       });
+       qrcode.makeCode(keyString);
+
+       let shortKeyFP = '';
+       if (key.fingerprint) {
+           shortKeyFP = PVE.Utils.render_pbs_fingerprint(key.fingerprint);
+       }
+
+       let printFrame = document.createElement("iframe");
+       Object.assign(printFrame.style, {
+           position: "fixed",
+           right: "0",
+           bottom: "0",
+           width: "0",
+           height: "0",
+           border: "0",
+       });
+       const prettifiedKey = JSON.stringify(key, null, 2);
+       const keyQrBase64 = qrdiv.children[0].toDataURL("image/png");
+       const html = `<html><head><script>
+           window.addEventListener('DOMContentLoaded', (ev) => window.print());
+       </script><style>@media print and (max-height: 150mm) {
+         h4, p { margin: 0; font-size: 1em; }
+       }</style></head><body style="padding: 5px;">
+       <h4>Encryption Key - Storage '${me.sid}' (${shortKeyFP})</h4>
+<p style="font-size:1.2em;font-family:monospace;white-space:pre-wrap;overflow-wrap:break-word;">
+-----BEGIN PROXMOX BACKUP KEY-----
+${prettifiedKey}
+-----END PROXMOX BACKUP KEY-----</p>
+       <center><img style="width: 100%; max-width: ${qrwidth}px;" src="${keyQrBase64}"></center>
+       </body></html>`;
+
+       printFrame.src = "data:text/html;base64," + btoa(html);
+       document.body.appendChild(printFrame);
+    },
+});
+
+Ext.define('PVE.panel.PBSEncryptionKeyTab', {
+    extend: 'Proxmox.panel.InputPanel',
+    xtype: 'pvePBSEncryptionKeyTab',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    onlineHelp: 'storage_pbs_encryption',
+
+    onGetValues: function(form) {
+       let values = {};
+       if (form.cryptMode === 'upload') {
+           values['encryption-key'] = form['crypt-key-upload'];
+       } else if (form.cryptMode === 'autogenerate') {
+           values['encryption-key'] = 'autogen';
+       } else if (form.cryptMode === 'none') {
+           if (!this.isCreate) {
+               values.delete = ['encryption-key'];
+           }
+       }
+       return values;
+    },
+
+    setValues: function(values) {
+       let me = this;
+       let vm = me.getViewModel();
+
+       let cryptKeyInfo = values['encryption-key'];
+       if (cryptKeyInfo) {
+           let icon = '<span class="fa fa-lock good"></span> ';
+           if (cryptKeyInfo.match(/^[a-fA-F0-9]{2}:/)) { // new style fingerprint
+               let shortKeyFP = PVE.Utils.render_pbs_fingerprint(cryptKeyInfo);
+               values['crypt-key-fp'] = icon + `${gettext('Active')} - ${gettext('Fingerprint')} ${shortKeyFP}`;
+           } else {
+               // old key without FP
+               values['crypt-key-fp'] = icon + gettext('Active');
+           }
+       } else {
+           values['crypt-key-fp'] = gettext('None');
+           let cryptModeNone = me.down('radiofield[inputValue=none]');
+           cryptModeNone.setBoxLabel(gettext('Do not encrypt backups'));
+           cryptModeNone.setValue(true);
+       }
+       vm.set('keepCryptVisible', !!cryptKeyInfo);
+       vm.set('allowEdit', !cryptKeyInfo);
+
+       me.callParent([values]);
+    },
+
+    viewModel: {
+       data: {
+           allowEdit: true,
+           keepCryptVisible: false,
+       },
+       formulas: {
+           showDangerousHint: get => {
+               let allowEdit = get('allowEdit');
+               return get('keepCryptVisible') && allowEdit;
+           },
+       },
+    },
+
+    items: [
+       {
+           xtype: 'displayfield',
+           name: 'crypt-key-fp',
+           fieldLabel: gettext('Encryption Key'),
+           padding: '2 0',
+       },
+       {
+           xtype: 'checkbox',
+           name: 'crypt-allow-edit',
+           boxLabel: gettext('Edit existing encryption key (dangerous!)'),
+           hidden: true,
+           submitValue: false,
+           isDirty: () => false,
+           bind: {
+               hidden: '{!keepCryptVisible}',
+               value: '{allowEdit}',
+           },
+       },
+       {
+           xtype: 'radiofield',
+           name: 'cryptMode',
+           inputValue: 'keep',
+           boxLabel: gettext('Keep encryption key'),
+           padding: '0 0 0 25',
+           cbind: {
+               hidden: '{isCreate}',
+               checked: '{!isCreate}',
+           },
+           bind: {
+               hidden: '{!keepCryptVisible}',
+               disabled: '{!allowEdit}',
+           },
+       },
+       {
+           xtype: 'radiofield',
+           name: 'cryptMode',
+           inputValue: 'none',
+           checked: true,
+           padding: '0 0 0 25',
+           cbind: {
+               disabled: '{!isCreate}',
+               checked: '{isCreate}',
+               boxLabel: get => get('isCreate')
+                   ? gettext('Do not encrypt backups')
+                   : gettext('Delete existing encryption key'),
+           },
+           bind: {
+               disabled: '{!allowEdit}',
+           },
+       },
+       {
+           xtype: 'radiofield',
+           name: 'cryptMode',
+           inputValue: 'autogenerate',
+           boxLabel: gettext('Auto-generate a client encryption key'),
+           padding: '0 0 0 25',
+           cbind: {
+               disabled: '{!isCreate}',
+           },
+           bind: {
+               disabled: '{!allowEdit}',
+           },
+       },
+       {
+           xtype: 'radiofield',
+           name: 'cryptMode',
+           inputValue: 'upload',
+           boxLabel: gettext('Upload an existing client encryption key'),
+           padding: '0 0 0 25',
+           cbind: {
+               disabled: '{!isCreate}',
+           },
+           bind: {
+               disabled: '{!allowEdit}',
+           },
+           listeners: {
+               change: function(f, value) {
+                   let panel = this.up('inputpanel');
+                   if (!panel.rendered) {
+                       return;
+                   }
+                   let uploadKeyField = panel.down('field[name=crypt-key-upload]');
+                   uploadKeyField.setDisabled(!value);
+                   uploadKeyField.setHidden(!value);
+
+                   let uploadKeyButton = panel.down('filebutton[name=crypt-upload-button]');
+                   uploadKeyButton.setDisabled(!value);
+                   uploadKeyButton.setHidden(!value);
+
+                   if (value) {
+                       uploadKeyField.validate();
+                   } else {
+                       uploadKeyField.reset();
+                   }
+               },
+           },
+       },
+       {
+           xtype: 'fieldcontainer',
+           layout: 'hbox',
+           items: [
+               {
+                   xtype: 'proxmoxtextfield',
+                   name: 'crypt-key-upload',
+                   fieldLabel: gettext('Key'),
+                   value: '',
+                   disabled: true,
+                   hidden: true,
+                   allowBlank: false,
+                   labelAlign: 'right',
+                   flex: 1,
+                   emptyText: gettext('You can drag-and-drop a key file here.'),
+                   validator: function(value) {
+                       if (value.length) {
+                           let key;
+                           try {
+                               key = JSON.parse(value);
+                           } catch (e) {
+                               return "Failed to parse key - " + e;
+                           }
+                           if (typeof key.data === undefined) {
+                               return "Does not seems like a valid Proxmox Backup key!";
+                           }
+                       }
+                       return true;
+                   },
+                   afterRender: function() {
+                       let field = this;
+                       if (!window.FileReader) {
+                           // No FileReader support in this browser
+                           return;
+                       }
+                       let cancel = function(ev) {
+                           ev = ev.event;
+                           if (ev.preventDefault) {
+                               ev.preventDefault();
+                           }
+                       };
+                       field.inputEl.on('dragover', cancel);
+                       field.inputEl.on('dragenter', cancel);
+                       field.inputEl.on('drop', function(ev) {
+                           ev = ev.event;
+                           if (ev.preventDefault) {
+                               ev.preventDefault();
+                           }
+                           let files = ev.dataTransfer.files;
+                           PVE.Utils.loadTextFromFile(files[0], v => field.setValue(v));
+                       });
+                   },
+               },
+               {
+                   xtype: 'filebutton',
+                   name: 'crypt-upload-button',
+                   iconCls: 'fa fa-fw fa-folder-open-o x-btn-icon-el-default-toolbar-small',
+                   cls: 'x-btn-default-toolbar-small proxmox-inline-button',
+                   margin: '0 0 0 4',
+                   disabled: true,
+                   hidden: true,
+                   listeners: {
+                       change: function(btn, e, value) {
+                           let ev = e.event;
+                           let field = btn.up().down('proxmoxtextfield[name=crypt-key-upload]');
+                           PVE.Utils.loadTextFromFile(ev.target.files[0], v => field.setValue(v));
+                           btn.reset();
+                       },
+                   },
+               },
+           ],
+       },
+       {
+           xtype: 'component',
+           border: false,
+           padding: '5 2',
+           userCls: 'pmx-hint',
+           html: // `<b style="color:red;font-weight:600;">${gettext('Warning')}</b>: ` +
+             `<span class="fa fa-exclamation-triangle" style="color:red;font-size:14px;"></span> ` +
+             gettext('Deleting or replacing the encryption key will break restoring backups created with it!'),
+           hidden: true,
+           bind: {
+               hidden: '{!showDangerousHint}',
+           },
+       },
+    ],
+});
+
 Ext.define('PVE.storage.PBSInputPanel', {
     extend: 'PVE.panel.StorageBase',
 
-    //onlineHelp: 'storage_pbs',
+    onlineHelp: 'storage_pbs',
+
+    apiCallDone: function(success, response, options) {
+       let res = response.result.data;
+       if (!(res && res.config && res.config['encryption-key'])) {
+           return;
+       }
+       let key = res.config['encryption-key'];
+       Ext.create('PVE.Storage.PBSKeyShow', {
+           autoShow: true,
+           sid: res.storage,
+           key: key,
+       });
+    },
+
+    isPBS: true, // HACK
+
+    extraTabs: [
+       {
+           xtype: 'pvePBSEncryptionKeyTab',
+           title: gettext('Encryption'),
+       },
+    ],
 
     initComponent: function() {
        var me = this;
@@ -32,28 +486,11 @@ Ext.define('PVE.storage.PBSInputPanel', {
                value: me.isCreate ? '' : '********',
                emptyText: me.isCreate ? gettext('None') : '',
                fieldLabel: gettext('Password'),
-               minLength: 5,
-           },
-           {
-               xtype: me.isCreate ? 'textfield' : 'displayfield',
-               name: 'datastore',
-               value: '',
-               fieldLabel: 'Datastore',
                allowBlank: false,
            },
        ];
 
        me.column2 = [
-           {  // FIXME: prune settings
-               xtype: 'proxmoxintegerfield',
-               fieldLabel: gettext('Max Backups'),
-               name: 'maxfiles',
-               reference: 'maxfiles',
-               minValue: 0,
-               maxValue: 365,
-               value: me.isCreate ? '0' : undefined,
-               allowBlank: false,
-           },
            {
                xtype: 'displayfield',
                name: 'content',
@@ -61,24 +498,26 @@ Ext.define('PVE.storage.PBSInputPanel', {
                submitValue: true,
                fieldLabel: gettext('Content'),
            },
+           {
+               xtype: me.isCreate ? 'textfield' : 'displayfield',
+               name: 'datastore',
+               value: '',
+               fieldLabel: 'Datastore',
+               allowBlank: false,
+           },
        ];
 
        me.columnB = [
            {
-               xtype: 'textfield',
+               xtype: 'proxmoxtextfield',
                name: 'fingerprint',
-               value: me.isCreate ? '' : undefined,
+               value: me.isCreate ? null : undefined,
                fieldLabel: gettext('Fingerprint'),
-               emptyText: gettext(`Server certificate SHA-256 fingerprint, required for self-signed certificates`),
+               emptyText: gettext('Server certificate SHA-256 fingerprint, required for self-signed certificates'),
                regex: /[A-Fa-f0-9]{2}(:[A-Fa-f0-9]{2}){31}/,
                regexText: gettext('Example') + ': AB:CD:EF:...',
                allowBlank: true,
            },
-           {
-               xtype: 'displayfield',
-               userCls: 'pmx-hint',
-               value: `Proxmox Backup Server is currently in beta, use with caution.`,
-           }
        ];
 
        me.callParent();