diff --git a/dashboard/src/constants.js b/dashboard/src/constants.js index bce51855a..4675af200 100644 --- a/dashboard/src/constants.js +++ b/dashboard/src/constants.js @@ -119,8 +119,6 @@ const ENDPOINTS_OVH = [ { name: 'Kimsufi North-America', value: 'kimsufi-ca' }, ]; -const SECRET_PLACEHOLDER = String.fromCharCode(0x25CF).repeat(8); - // List is from http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region const REGIONS_S3 = [ { name: 'Africa (Cape Town)', value: 'af-south-1' }, @@ -334,7 +332,6 @@ export { PROXY_APP_ID, TOKEN_TYPES, ENDPOINTS_OVH, - SECRET_PLACEHOLDER, STORAGE_PROVIDERS, BACKUP_FORMATS, REGIONS_CONTABO, @@ -366,7 +363,6 @@ export default { PROXY_APP_ID, TOKEN_TYPES, ENDPOINTS_OVH, - SECRET_PLACEHOLDER, STORAGE_PROVIDERS, BACKUP_FORMATS, REGIONS_CONTABO, diff --git a/dashboard/src/views/VolumesView.vue b/dashboard/src/views/VolumesView.vue index ec40bbc2c..fa9f91772 100644 --- a/dashboard/src/views/VolumesView.vue +++ b/dashboard/src/views/VolumesView.vue @@ -5,7 +5,7 @@ const i18n = useI18n(); const t = i18n.t; import { computed, ref, useTemplateRef, onMounted } from 'vue'; -import { Button, Menu, Checkbox, Dialog, SingleSelect, FormGroup, InputDialog, NumberInput, PasswordInput, TableView, TextInput } from '@cloudron/pankow'; +import { Button, Menu, Checkbox, Dialog, SingleSelect, FormGroup, InputDialog, NumberInput, TableView, TextInput, MaskedInput } from '@cloudron/pankow'; import Section from '../components/Section.vue'; import StateLED from '../components/StateLED.vue'; import VolumesModel from '../models/VolumesModel.js'; @@ -173,9 +173,19 @@ async function openVolumeDialog(volume) { volumeDialogData.value.xfsBlockDevices = xfsBlockDevices; volumeDialog.value.open(); + + setTimeout(checkValidity, 100); // update state of the confirm button } -async function submitVolumeDialog() { +const form = useTemplateRef('form'); +const isFormValid = ref(false); +function checkValidity() { + isFormValid.value = form.value.checkValidity(); +} + +async function onSubmit() { + if (!form.value.reportValidity()) return; + volumeDialogData.value.busy = true; const mountOptions = { @@ -185,11 +195,11 @@ async function submitVolumeDialog() { remoteDir: volumeDialogData.value.remoteDir, user: volumeDialogData.value.user, username: volumeDialogData.value.username, - password: volumeDialogData.value.password, diskPath: volumeDialogData.value.diskPath, hostPath: volumeDialogData.value.hostPath, - privateKey: volumeDialogData.value.privateKey, }; + if (volumeDialogData.value.password) mountOptions.password = volumeDialogData.value.password; // cifs + if (volumeDialogData.value.privateKey) mountOptions.privateKey = volumeDialogData.value.privateKey; // sshfs let error; if (volumeDialogData.value.mode === 'new') { @@ -269,11 +279,11 @@ onMounted(async () =>{ :reject-label="$t('main.dialog.cancel')" reject-style="secondary" :confirm-label="$t('main.dialog.save')" - :confirm-active="volumeDialogValid" + :confirm-active="!volumeDialogData.busy && isFormValid" :confirm-busy="volumeDialogData.busy" - @confirm="submitVolumeDialog()" + @confirm="onSubmit()" > -
+
@@ -286,7 +296,7 @@ onMounted(async () =>{ - + @@ -324,7 +334,7 @@ onMounted(async () =>{ - + @@ -335,7 +345,7 @@ onMounted(async () =>{ - +
diff --git a/src/routes/volumes.js b/src/routes/volumes.js index 6e39b3d31..22ef0f56e 100644 --- a/src/routes/volumes.js +++ b/src/routes/volumes.js @@ -1,16 +1,5 @@ 'use strict'; -exports = module.exports = { - add, - get, - update, - del, - list, - load, - remount, - getStatus -}; - const assert = require('node:assert'), AuditSource = require('../auditsource.js'), BoxError = require('../boxerror.js'), @@ -56,7 +45,7 @@ async function update(req, res, next) { if (!req.body.mountOptions || typeof req.body.mountOptions !== 'object') return next(new HttpError(400, 'mountOptions must be a non-null object')); - const [error] = await safe(volumes.update(req.resources.volume.id, req.body.mountOptions, AuditSource.fromRequest(req))); + const [error] = await safe(volumes.update(req.resources.volume, req.body.mountOptions, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(204)); @@ -94,3 +83,14 @@ async function getStatus(req, res, next) { if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, status)); } + +exports = module.exports = { + add, + get, + update, + del, + list, + load, + remount, + getStatus +}; diff --git a/src/volumes.js b/src/volumes.js index c33a31755..f580432f4 100644 --- a/src/volumes.js +++ b/src/volumes.js @@ -1,24 +1,7 @@ 'use strict'; -exports = module.exports = { - add, - get, - update, - del, - list, - remount, - getStatus, - removePrivateFields, - - mountAll, - - // exported for testing - _validateHostPath: validateHostPath -}; - const assert = require('node:assert'), BoxError = require('./boxerror.js'), - constants = require('./constants.js'), crypto = require('node:crypto'), database = require('./database.js'), debug = require('debug')('box:volumes'), @@ -126,40 +109,51 @@ async function getStatus(volume) { return await mounts.getStatus(volume.mountType, volume.hostPath); // { state, message } } +async function get(id) { + assert.strictEqual(typeof id, 'string'); + + const result = await database.query(`SELECT ${VOLUMES_FIELDS} FROM volumes WHERE id=?`, [ id ]); + if (result.length === 0) return null; + + return postProcess(result[0]); +} + function removePrivateFields(volume) { const newVolume = Object.assign({}, volume); if (newVolume.mountType === mounts.MOUNT_TYPE_SSHFS) { - newVolume.mountOptions.privateKey = constants.SECRET_PLACEHOLDER; + delete newVolume.mountOptions.privateKey; } else if (newVolume.mountType === mounts.MOUNT_TYPE_CIFS) { - newVolume.mountOptions.password = constants.SECRET_PLACEHOLDER; + delete newVolume.mountOptions.password; } return newVolume; } +function injectPrivateFields(newVolume, currentVolume) { + if (currentVolume.mountType === mounts.MOUNT_TYPE_SSHFS) { + if (!Object.hasOwn(newVolume.mountOptions, 'privateKey')) newVolume.mountOptions.privateKey = currentVolume.mountOptions.privateKey; + } else if (currentVolume.mountType === mounts.MOUNT_TYPE_CIFS) { + if (!Object.hasOwn(newVolume.mountOptions, 'password')) newVolume.mountOptions.password = currentVolume.mountOptions.password; + } +} + // only network mounts can be updated here through mountOptions to update logon information -async function update(id, mountOptions, auditSource) { - assert.strictEqual(typeof id, 'string'); +async function update(volume, mountOptions, auditSource) { + assert.strictEqual(typeof volume, 'object'); assert.strictEqual(typeof mountOptions, 'object'); assert.strictEqual(typeof auditSource, 'object'); - const volume = await get(id); const { name, mountType } = volume; if (mountType !== mounts.MOUNT_TYPE_CIFS && mountType !== mounts.MOUNT_TYPE_SSHFS && mountType !== mounts.MOUNT_TYPE_NFS) throw new BoxError(BoxError.BAD_FIELD, 'Only network mounts can be updated'); + injectPrivateFields({ name, mountType, mountOptions }, volume); + const error = await mounts.validateMountOptions(mountType, mountOptions); if (error) throw error; - // put old secret back in place if no new secret is provided - if (mountType === mounts.MOUNT_TYPE_SSHFS) { - if (mountOptions.privateKey === constants.SECRET_PLACEHOLDER) mountOptions.privateKey = volume.mountOptions.privateKey; - } else if (mountType === mounts.MOUNT_TYPE_CIFS) { - if (mountOptions.password === constants.SECRET_PLACEHOLDER) mountOptions.password = volume.mountOptions.password; - } - - const hostPath = path.join(paths.VOLUMES_MOUNT_DIR, id); + const hostPath = path.join(paths.VOLUMES_MOUNT_DIR, volume.id); const mount = { description: name, hostPath, mountType, mountOptions }; // first try to mount at /mnt/volumes/-validation @@ -169,21 +163,12 @@ async function update(id, mountOptions, auditSource) { // update the mount await mounts.tryAddMount(mount, { timeout: 10 }); // 10 seconds - await database.query('UPDATE volumes SET hostPath=?,mountOptionsJson=? WHERE id=?', [ hostPath, JSON.stringify(mountOptions), id ]); - await eventlog.add(eventlog.ACTION_VOLUME_UPDATE, auditSource, { id, name, hostPath }); + await database.query('UPDATE volumes SET hostPath=?,mountOptionsJson=? WHERE id=?', [ hostPath, JSON.stringify(mountOptions), volume.id ]); + await eventlog.add(eventlog.ACTION_VOLUME_UPDATE, auditSource, { id: volume.id, name, hostPath }); // in theory, we only need to do this mountpoint volumes. but for some reason a restart is required to detect new "mounts" safe(services.rebuildService('sftp', auditSource), { debug }); } -async function get(id) { - assert.strictEqual(typeof id, 'string'); - - const result = await database.query(`SELECT ${VOLUMES_FIELDS} FROM volumes WHERE id=?`, [ id ]); - if (result.length === 0) return null; - - return postProcess(result[0]); -} - async function list() { const results = await database.query(`SELECT ${VOLUMES_FIELDS} FROM volumes ORDER BY name`); results.forEach(postProcess); @@ -216,3 +201,19 @@ async function mountAll() { await mounts.tryAddMount(mount, { timeout: 10, skipCleanup: true }); // have to wait to avoid race with apptask } } + +exports = module.exports = { + add, + get, + update, + del, + list, + remount, + getStatus, + removePrivateFields, + + mountAll, + + // exported for testing + _validateHostPath: validateHostPath +};