From 6e1842a412d00b27f084abc1c6952d36accd49fa Mon Sep 17 00:00:00 2001 From: Viacheslav Okhmak Date: Wed, 13 Sep 2023 15:37:36 +0300 Subject: [PATCH] Fixes from s-pro (#349) * fix radio and checkbox inputs for document builder flow * fix unnecessary requests for user data on frontend * fix login with metamask * add validation for the user session cookie * fix wrong formatting --- main/handlers/api/handlers.go | 10 +- main/handlers/routes.go | 1 + ui/core/src/baseApp.js | 40 ++-- .../src/components/user/TopRightProfile.vue | 1 + ui/core/src/libs/legacy/global.js | 176 +++++++++++++----- ui/core/src/views/AdminLogin.vue | 47 +++-- 6 files changed, 203 insertions(+), 72 deletions(-) diff --git a/main/handlers/api/handlers.go b/main/handlers/api/handlers.go index ec4e82e5a..a6f3ea2f0 100644 --- a/main/handlers/api/handlers.go +++ b/main/handlers/api/handlers.go @@ -632,6 +632,14 @@ func LoginHandler(e echo.Context) (err error) { }) } +// Validates user session cookie +func ValidateUserSession(e echo.Context) (err error) { + c := e.(*www.Context) + c.Session(true) + + return c.NoContent(http.StatusOK) +} + // Returns an object containing // // { @@ -1133,7 +1141,7 @@ func GetProfilePhotoHandler(e echo.Context) error { err := userService.GetProfilePhoto(sess, id, c.Response().Writer) if err != nil { - return c.NoContent(http.StatusNotFound) + return c.NoContent(http.StatusNoContent) } c.Response().Committed = true c.Response().Header().Set("Content-Type", "image/jpeg") diff --git a/main/handlers/routes.go b/main/handlers/routes.go index a8d354d8c..ff00873dc 100644 --- a/main/handlers/routes.go +++ b/main/handlers/routes.go @@ -66,6 +66,7 @@ func MainHostedAPI(e *echo.Echo, s *www.Security, version string) { {GET, PUBLIC, "/api/config", api.ConfigHandler(version)}, // authentication + {GET, PUBLIC, "/api/session/validate", api.ValidateUserSession}, {GET, PUBLIC, "/api/session/token", api.GetSessionTokenHandler}, {DELETE, USER, "/api/session/token", api.DeleteSessionTokenHandler}, {GET, PUBLIC, "/api/challenge", api.ChallengeHandler}, // Need session diff --git a/ui/core/src/baseApp.js b/ui/core/src/baseApp.js index 0d7e1b687..4db360309 100644 --- a/ui/core/src/baseApp.js +++ b/ui/core/src/baseApp.js @@ -73,15 +73,13 @@ export default { if (this.me.role >= 100) { return true } - } catch (e) { - } + } catch (e) {} try { // check public if (item.publicByID[0] === 2) { return true } - } catch (e) { - } + } catch (e) {} try { // check owner @@ -92,8 +90,7 @@ export default { item.grant[this.me.id][0] === 2) { return true } - } catch (e) { - } + } catch (e) {} try { // check group if (this.me.role !== 0) { @@ -102,8 +99,7 @@ export default { return true } } - } catch (e) { - } + } catch (e) {} // check others try { @@ -111,8 +107,7 @@ export default { // others have write rights return true } - } catch (e) { - } + } catch (e) {} return false }, handleError (o) { @@ -152,14 +147,29 @@ export default { }, setSelectedLang (lang) { if (lang) { - this.$cookie.set('lang', lang, { expires: '1Y' }) + this.$cookie.set('lang', lang, { + expires: '1Y' + }) this.reloadI18n() } else { this.$cookie.delete('lang') this.$i18n.set(this.fallbackLang()) } }, + checkUserHasSession () { + return !!localStorage.getItem('userhassession') + }, + initUserHasSession () { + localStorage.setItem('userhassession', true) + }, + deleteUserHasSession () { + localStorage.removeItem('userhassession') + }, loadMe (clb) { + if (!this.checkUserHasSession()) { + return + } + axios.get('/api/me').then((response) => { this.me = response.data this.$root.$emit('me', this.me) @@ -171,6 +181,7 @@ export default { } } }, (err) => { + this.deleteUserHasSession() this.handleError(err) }) }, @@ -305,6 +316,9 @@ export default { this.handleError(err) }) }, + async validateSessionCookie () { + return axios.get('/api/session/validate') + }, loadMeta (clb) { axios.get('/api/i18n/meta').then((response) => { this.meta = response.data @@ -396,14 +410,14 @@ export default { get () { return this.$root.$children[0] }, - set (a) { - } + set (a) {} } }, created () { const tmpLangToPreventFromWarnings = 'en' this.$i18n.fallback(tmpLangToPreventFromWarnings) this.$i18n.set(tmpLangToPreventFromWarnings) + this.validateSessionCookie() this.loadMeta() this.loadConfig() this.loadMe() diff --git a/ui/core/src/components/user/TopRightProfile.vue b/ui/core/src/components/user/TopRightProfile.vue index 6a62dc1d9..7a9da6224 100644 --- a/ui/core/src/components/user/TopRightProfile.vue +++ b/ui/core/src/components/user/TopRightProfile.vue @@ -172,6 +172,7 @@ export default { }, logout () { axios.post('/api/logout', null).then(response => { + this.app.deleteUserHasSession() window.location.replace('/') }, (err) => { this.app.handleError(err) diff --git a/ui/core/src/libs/legacy/global.js b/ui/core/src/libs/legacy/global.js index 669707bd1..3a2370511 100644 --- a/ui/core/src/libs/legacy/global.js +++ b/ui/core/src/libs/legacy/global.js @@ -86,7 +86,8 @@ var cglbl = { return this.scrollBarWith }, sizeOf: function (obj) { - var size = 0; var key + var size = 0; + var key for (key in obj) { if (obj.hasOwnProperty(key)) size++ } @@ -203,7 +204,9 @@ $.fn.checkAndFillInputValue = function (fullKey, dataVal, element) { _.parents('.field-parent').attr('data-status', 'success') _.attr('data-status', 'success') _.doCompAction(true) - _.trigger('change', [{ init: true }]) + _.trigger('change', [{ + init: true + }]) return true } } else if (_.attr('type') === 'checkbox') { @@ -214,7 +217,9 @@ $.fn.checkAndFillInputValue = function (fullKey, dataVal, element) { _.parents('.field-parent').attr('data-status', 'success') _.attr('data-status', 'success') _.doCompAction(true) - _.trigger('change', [{ init: true }]) + _.trigger('change', [{ + init: true + }]) return true } else { // multi _.removeAttr('checked') @@ -223,7 +228,9 @@ $.fn.checkAndFillInputValue = function (fullKey, dataVal, element) { _.parents('.field-parent').attr('data-status', 'success') _.attr('data-status', 'success') _.doCompAction(true) - _.trigger('change', [{ init: true }]) + _.trigger('change', [{ + init: true + }]) return true } } @@ -268,7 +275,9 @@ $.fn.checkAndFillInputValue = function (fullKey, dataVal, element) { _.attr('data-status', 'success') _.doCompAction(true) if (_.is('select')) { - _.trigger('change', [{ init: true }]) + _.trigger('change', [{ + init: true + }]) } return true } else { @@ -353,7 +362,9 @@ FTGlobal.prototype.createDatepicker = function (elem, options) { pickr.setDate(elem.val()) } } - } catch (e) { console.log(e) } + } catch (e) { + console.log(e) + } } catch (dateExc) { console.log(dateExc) } @@ -438,7 +449,7 @@ $.fn.nextParentWithClass = function (attrClass) { var parent = this while (!parent.hasClass(attrClass)) { parent = parent.parent() - ++index + ++index if (maxIndex < index) { break } @@ -608,9 +619,9 @@ $.fn.fileElement = function (element) { } jQuery.fn.center = function () { this.css('margin-top', Math.max(0, (($(window).height() - $(this).outerHeight()) / 2) + - $(window).scrollTop()) + 'px') + $(window).scrollTop()) + 'px') this.css('margin-left', Math.max(0, (($(window).width() - $(this).outerWidth()) / 2) + - $(window).scrollLeft()) + 'px') + $(window).scrollLeft()) + 'px') return this } @@ -726,7 +737,9 @@ $.fn.assignSubmitOnChange = function (options) { if ($.isFunction(options.success)) { options.success.apply(this, [data, textStatus, xhr, this.myReq]) } - } catch (eee) { console.log(eee) } + } catch (eee) { + console.log(eee) + } }, error: function (data, a2, a3, a4) { var status = 'error' @@ -738,13 +751,20 @@ $.fn.assignSubmitOnChange = function (options) { this.responsibleEl.showFieldErrors(data.responseJSON) } if (data.status == 502) { - this.responsibleEl.showFieldErrors({ errors: [{ field: this.responsibleEl.attr('name'), message: FTG.translate('file.limit.exceeded') }] }) + this.responsibleEl.showFieldErrors({ + errors: [{ + field: this.responsibleEl.attr('name'), + message: FTG.translate('file.limit.exceeded') + }] + }) } try { if ($.isFunction(options.error)) { options.error.apply(this, [data, a2, a3, this.myReq]) } - } catch (eee) { console.log(eee) } + } catch (eee) { + console.log(eee) + } } }) } @@ -950,7 +970,7 @@ $.fn.serializeFormToObject = function (excludeFileFormFields) { for (var i = 0; i < allElementsHavingNameAttr.length; ++i) { targetElement = $(allElementsHavingNameAttr[i]) // if (excludeFileFormFields === true) { - if (!targetElement.is(':visible') || targetElement.attr('type') === 'file') { + if (targetElement.attr('type') === 'file') { continue } // } @@ -1008,7 +1028,9 @@ $.fn.isValAsFilenameValid = function () { $.fn.makeSameHeight = function (options) { var _this = this if (!options) { - options = { delay: 10 } + options = { + delay: 10 + } } if (!options.delay) { options.delay = 10 @@ -1045,35 +1067,103 @@ String.prototype.msgFormat = function () { }) } jQuery.fn.copyHtmlToClipboard = function () { - if (this.length === 0) { - return this - } - var node = this[0] - try { - var range, selection - selection = window.getSelection() - range = document.createRange() - range.selectNodeContents(node) - selection.removeAllRanges() - selection.addRange(range) - document.execCommand('copy') - $('#copyToclipboardInfo_').remove() - var cInfo = $('copied to clipboard') - $(node).append(cInfo) - setTimeout(function () { - cInfo.remove() - }, 4000) - } catch (e) { - console.log(e) + if (this.length === 0) { + return this + } + var node = this[0] + try { + var range, selection + selection = window.getSelection() + range = document.createRange() + range.selectNodeContents(node) + selection.removeAllRanges() + selection.addRange(range) + document.execCommand('copy') + $('#copyToclipboardInfo_').remove() + var cInfo = $('copied to clipboard') + $(node).append(cInfo) + setTimeout(function () { + cInfo.remove() + }, 4000) + } catch (e) { + console.log(e) + } } -} -/*! - * JavaScript Cookie v2.1.2 - * https://github.com/js-cookie/js-cookie - * - * Copyright 2006, 2015 Klaus Hartl & Fagner Brack - * Released under the MIT license - */ -!(function (e) { if (typeof define === 'function' && define.amd)define(e); else if (typeof exports === 'object')module.exports = e(); else { var n = window.Cookies; var t = window.Cookies = e(); t.noConflict = function () { return window.Cookies = n, t } } }(function () { function e () { for (var e = 0, n = {}; e < arguments.length; e++) { var t = arguments[e]; for (var o in t)n[o] = t[o] } return n } function n (t) { function o (n, r, i) { var c; if (typeof document !== 'undefined') { if (arguments.length > 1) { if (i = e({ path: '/' }, o.defaults, i), typeof i.expires === 'number') { var a = new Date(); a.setMilliseconds(a.getMilliseconds() + 864e5 * i.expires), i.expires = a } try { c = JSON.stringify(r), /^[\{\[]/.test(c) && (r = c) } catch (s) {} return r = t.write ? t.write(r, n) : encodeURIComponent(String(r)).replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent), n = encodeURIComponent(String(n)), n = n.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent), n = n.replace(/[\(\)]/g, escape), document.cookie = [n, '=', r, i.expires ? '; expires=' + i.expires.toUTCString() : '', i.path ? '; path=' + i.path : '', i.domain ? '; domain=' + i.domain : '', i.secure ? '; secure' : ''].join('') }n || (c = {}); for (var p = document.cookie ? document.cookie.split('; ') : [], u = /(%[0-9A-Z]{2})+/g, d = 0; d < p.length; d++) { var f = p[d].split('='); var l = f.slice(1).join('='); l.charAt(0) === '"' && (l = l.slice(1, -1)); try { var m = f[0].replace(u, decodeURIComponent); if (l = t.read ? t.read(l, m) : t(l, m) || l.replace(u, decodeURIComponent), this.json) try { l = JSON.parse(l) } catch (s) {} if (n === m) { c = l; break }n || (c[m] = l) } catch (s) {} } return c } } return o.set = o, o.get = function (e) { return o(e) }, o.getJSON = function () { return o.apply({ json: !0 }, [].slice.call(arguments)) }, o.defaults = {}, o.remove = function (n, t) { o(n, '', e(t, { expires: -1 })) }, o.withConverter = n, o } return n(function () {}) })) + /*! + * JavaScript Cookie v2.1.2 + * https://github.com/js-cookie/js-cookie + * + * Copyright 2006, 2015 Klaus Hartl & Fagner Brack + * Released under the MIT license + */ + !(function (e) { + if (typeof define === 'function' && define.amd) define(e); + else if (typeof exports === 'object') module.exports = e(); + else { + var n = window.Cookies; + var t = window.Cookies = e(); + t.noConflict = function () { + return window.Cookies = n, t + } + } + }(function () { + function e() { + for (var e = 0, n = {}; e < arguments.length; e++) { + var t = arguments[e]; + for (var o in t) n[o] = t[o] + } + return n + } + + function n(t) { + function o(n, r, i) { + var c; + if (typeof document !== 'undefined') { + if (arguments.length > 1) { + if (i = e({ + path: '/' + }, o.defaults, i), typeof i.expires === 'number') { + var a = new Date(); + a.setMilliseconds(a.getMilliseconds() + 864e5 * i.expires), i.expires = a + } + try { + c = JSON.stringify(r), /^[\{\[]/.test(c) && (r = c) + } catch (s) {} + return r = t.write ? t.write(r, n) : encodeURIComponent(String(r)).replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent), n = encodeURIComponent(String(n)), n = n.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent), n = n.replace(/[\(\)]/g, escape), document.cookie = [n, '=', r, i.expires ? '; expires=' + i.expires.toUTCString() : '', i.path ? '; path=' + i.path : '', i.domain ? '; domain=' + i.domain : '', i.secure ? '; secure' : ''].join('') + } + n || (c = {}); + for (var p = document.cookie ? document.cookie.split('; ') : [], u = /(%[0-9A-Z]{2})+/g, d = 0; d < p.length; d++) { + var f = p[d].split('='); + var l = f.slice(1).join('='); + l.charAt(0) === '"' && (l = l.slice(1, -1)); + try { + var m = f[0].replace(u, decodeURIComponent); + if (l = t.read ? t.read(l, m) : t(l, m) || l.replace(u, decodeURIComponent), this.json) try { + l = JSON.parse(l) + } catch (s) {} + if (n === m) { + c = l; + break + } + n || (c[m] = l) + } catch (s) {} + } + return c + } + } + return o.set = o, o.get = function (e) { + return o(e) + }, o.getJSON = function () { + return o.apply({ + json: !0 + }, [].slice.call(arguments)) + }, o.defaults = {}, o.remove = function (n, t) { + o(n, '', e(t, { + expires: -1 + })) + }, o.withConverter = n, o + } + return n(function () {}) + })) export default FTG diff --git a/ui/core/src/views/AdminLogin.vue b/ui/core/src/views/AdminLogin.vue index e0545232b..e9eedafd4 100644 --- a/ui/core/src/views/AdminLogin.vue +++ b/ui/core/src/views/AdminLogin.vue @@ -94,9 +94,11 @@ export default { this.pwlogin = true if (this.checkTermsAndConditions()) { axios.post('/api/login', { email: this.email, password: this.password }).then(res => { + this.app.initUserHasSession() this.hasError = false window.location = res.data.location || '/admin/workflow' }, (err) => { + this.app.deleteUserHasSession() this.app.handleError(err) this.loginErrorMessage = this.$t('You have entered an invalid username or password') this.hasError = true @@ -135,6 +137,7 @@ export default { this.challenge = response.data this.metamaskLogin() }, (err) => { + this.app.deleteUserHasSession() this.app.handleError(err) }) } @@ -149,36 +152,50 @@ export default { await this.app.wallet.wallet.setupDefaultAccount() } catch (e) { console.log(e) + this.app.deleteUserHasSession() this.walletErrorMessage = this.$t('Please grant access to MetaMask.') return } } else { + this.app.deleteUserHasSession() this.walletErrorMessage = this.$t('Please grant access to MetaMask.') return } + this.account = this.app.wallet.getCurrentAddress() + if (this.account === undefined) { + this.app.deleteUserHasSession() this.walletErrorMessage = this.$t('Please sign in to MetaMask.') return } + this.pwlogin = false + if (this.checkTermsAndConditions()) { - this.app.wallet.signMessage(this.challenge, this.account).then((signature) => { - axios.post('/api/login', { signature }).then((res) => { - this.challenge = '' - if (res.status >= 200 && res.status <= 299) { - window.location = res.data.location || '/admin/workflow' - } else { - this.walletErrorMessage = this.$t('Could not verify signature.') - } - }, (err) => { - this.challenge = '' - this.app.handleError(err) - this.walletErrorMessage = this.$t('Could not verify signature.') + this.app.wallet.signMessage(this.challenge, this.account) + .then((signature) => { + axios.post('/api/login', { signature }) + .then((res) => { + this.challenge = '' + + if (res.status >= 200 && res.status <= 299) { + this.app.initUserHasSession() + window.location = res.data.location || '/admin/workflow' + } else { + this.app.deleteUserHasSession() + this.walletErrorMessage = this.$t('Could not verify signature.') + } + }, (err) => { + this.challenge = '' + this.app.deleteUserHasSession() + this.app.handleError(err) + this.walletErrorMessage = this.$t('Could not verify signature.') + }) + }).catch(() => { + this.app.deleteUserHasSession() + this.walletErrorMessage = this.$t('Could not Sign Message.') }) - }).catch(() => { - this.walletErrorMessage = this.$t('Could not Sign Message.') - }) } } }