From 0cae7e9eda3b62c17cfa7ec620913f4a504bc5ee Mon Sep 17 00:00:00 2001 From: Zephyrrus Date: Thu, 17 Jun 2021 16:06:53 +0300 Subject: [PATCH] feat: show validation errors from joi on the frontend --- src/api/routes/service/configPOST.js | 27 +++++++++++++++++---- src/api/structures/Setting.js | 6 ++--- src/api/utils/Util.js | 2 +- src/site/components/settings/JoiObject.vue | 16 ++++++++++-- src/site/pages/dashboard/admin/settings.vue | 23 ++++++++++++++---- src/site/store/admin.js | 1 - src/site/store/config.js | 18 +++++++------- 7 files changed, 67 insertions(+), 26 deletions(-) diff --git a/src/api/routes/service/configPOST.js b/src/api/routes/service/configPOST.js index 28d034d..9129950 100644 --- a/src/api/routes/service/configPOST.js +++ b/src/api/routes/service/configPOST.js @@ -1,19 +1,36 @@ -const Joi = require('joi'); - const Route = require('../../structures/Route'); const Util = require('../../utils/Util'); const { schema } = require('../../structures/Setting'); +const joiOptions = { + abortEarly: false, // include all errors + allowUnknown: true, // ignore unknown props + stripUnknown: true // remove unknown props +}; + class configGET extends Route { constructor() { super('/service/config', 'post', { adminOnly: true }); } - run(req, res) { + async run(req, res) { const { settings } = req.body; - const validationRes = schema.validate(settings, { abortEarly: false }); - console.log(JSON.stringify(validationRes)); + const { error, value } = schema.validate(settings, joiOptions); + if (error) { + return res.status(400).json({ + errors: error.details.reduce((acc, v) => { + for (const p of v.path) { + acc[p] = (acc[p] || []).concat(v.message); + } + return acc; + }, {}) + }); + } + + await Util.writeConfigToDb(value); + + return res.status(200).json({ value }); } } diff --git a/src/api/structures/Setting.js b/src/api/structures/Setting.js index ff98339..7650ccb 100644 --- a/src/api/structures/Setting.js +++ b/src/api/structures/Setting.js @@ -116,9 +116,9 @@ const schema = Joi.object({ .description('Allows people to create new accounts'), // Social and sharing - metaThemeColor: Joi.string().hex().min(3) - .max(6) - .default('20222b') + metaThemeColor: Joi.string().pattern(/^#([0-9a-f]{6}|[0-9a-f]{3})$/i).min(4) + .max(7) + .default('#20222b') .meta({ section: Sections.SOCIAL_AND_SHARING }) diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js index 3780460..628be82 100644 --- a/src/api/utils/Util.js +++ b/src/api/utils/Util.js @@ -64,7 +64,7 @@ class Util { static async writeConfigToDb(config) { // TODO: Check that the config passes the joi schema validation - if (!config || !config.key || !config.key) return; + if (!config || !config.key) return; try { config.value = JSON.stringify(config.value); await db.table('settings').insert(config); diff --git a/src/site/components/settings/JoiObject.vue b/src/site/components/settings/JoiObject.vue index f77b249..8d3a803 100644 --- a/src/site/components/settings/JoiObject.vue +++ b/src/site/components/settings/JoiObject.vue @@ -3,7 +3,8 @@
@@ -108,6 +108,14 @@ export default { return [...acc, ...item.allow]; }, []); }, + getValidationType(fieldName) { + if (Array.isArray(this.errors[fieldName])) return 'is-danger'; + return null; + }, + getErrorMessage(fieldName) { + if (Array.isArray(this.errors[fieldName])) return this.errors[fieldName].join('\n'); + return null; + }, getValues() { return this.values; } @@ -119,6 +127,10 @@ export default { .field { margin-bottom: 1em; + + ::v-deep .help.is-danger { + white-space: pre-line; + } } .taginp { diff --git a/src/site/pages/dashboard/admin/settings.vue b/src/site/pages/dashboard/admin/settings.vue index 7346729..51e5ea8 100644 --- a/src/site/pages/dashboard/admin/settings.vue +++ b/src/site/pages/dashboard/admin/settings.vue @@ -15,7 +15,7 @@
{{ sectionName }}
- +
@@ -42,6 +42,11 @@ export default { JoiObject }, middleware: ['auth', 'admin'], + data() { + return { + validationErrors: {} + }; + }, computed: { ...mapState({ settings: state => state.admin.settings, @@ -74,16 +79,24 @@ export default { onConfirm: () => this.saveSettings() }); }, - saveSettings() { + async saveSettings() { // handle refs let settings = {}; for (const joiComponent of this.$refs.jois) { settings = { ...settings, ...joiComponent.getValues() }; } - this.$handler.executeAction('admin/saveSettings', settings); - // restart service - // this.$handler.executeAction('admin/restartService'); + try { + await this.$store.dispatch('admin/saveSettings', settings); + this.$set(this, 'validationErrors', {}); + + await this.$store.dispatch('config/fetchSettings'); + this.$handler.executeAction('admin/restartService'); + } catch (e) { + if (e.response?.data?.errors) { + this.$set(this, 'validationErrors', e.response.data.errors); + } + } } }, head() { diff --git a/src/site/store/admin.js b/src/site/store/admin.js index 0f946e9..9b1d591 100644 --- a/src/site/store/admin.js +++ b/src/site/store/admin.js @@ -28,7 +28,6 @@ export const actions = { }, async saveSettings({ commit }, settings) { const response = await this.$axios.$post('service/config', { settings }); - commit('setSettings', response); return response; }, diff --git a/src/site/store/config.js b/src/site/store/config.js index 7f6bcae..124b778 100644 --- a/src/site/store/config.js +++ b/src/site/store/config.js @@ -10,6 +10,15 @@ export const state = () => ({ userAccounts: false }); +export const actions = { + async fetchSettings({ commit }) { + const response = await this.$axios.$get('service/config'); + commit('setSettings', response); + + return response; + } +}; + export const mutations = { setSettings(state, { config }) { state.version = `v${config.version}`; @@ -22,12 +31,3 @@ export const mutations = { state.userAccounts = config.userAccounts; } }; - -export const actions = { - async fetchSettings({ commit }) { - const response = await this.$axios.$get('service/config'); - commit('setSettings', response); - - return response; - } -};