From 918936e591f2bab7d99da125e0e3716ff9d79816 Mon Sep 17 00:00:00 2001 From: Maxim Stogniy Date: Thu, 21 May 2020 17:38:21 +1200 Subject: [PATCH 01/19] Add authentication --- package.json | 13 +++++--- src/admin/components/Header.vue | 13 ++++++-- src/admin/components/Login.vue | 23 ++++++++------ src/admin/customAxios.js | 31 ++++++++++++++++++ src/admin/main.js | 9 +++--- src/admin/router/index.js | 39 +++++++++++++++++++++++ src/admin/router/routes.js | 52 ++++++++++++++++++++++++++++++ src/admin/routes.js | 51 ------------------------------ src/admin/store/index.js | 10 ++++++ src/admin/store/modules/auth.js | 29 +++++++++++++++++ webpack.config.js | 2 +- yarn.lock | 56 ++++++++++++++++++++++++++++----- 12 files changed, 247 insertions(+), 81 deletions(-) create mode 100644 src/admin/customAxios.js create mode 100644 src/admin/router/index.js create mode 100644 src/admin/router/routes.js delete mode 100644 src/admin/routes.js create mode 100644 src/admin/store/index.js create mode 100644 src/admin/store/modules/auth.js diff --git a/package.json b/package.json index 6732083..24c13e9 100644 --- a/package.json +++ b/package.json @@ -47,8 +47,8 @@ "svg-transform-loader": "^2.0.7", "svgo-loader": "^2.2.0", "terser-webpack-plugin": "^1.2.3", - "vue-loader": "^15.6.4", - "vue-template-compiler": "^2.6.7", + "vue-loader": "^15.9.2", + "vue-template-compiler": "^2.6.11", "webpack": "^4.29.5", "webpack-cli": "^3.2.3", "webpack-dev-server": "^3.2.0" @@ -57,14 +57,17 @@ "last 2 versions" ], "dependencies": { + "axios": "^0.19.2", + "babel-polyfill": "^6.26.0", "normalize.css": "^8.0.1", "promptly": "^3.0.3", "pug": "^2.0.3", "request": "^2.88.0", "signale": "^1.4.0", - "vue": "^2.6.7", + "vue": "^2.6.11", "vue-carousel": "^0.18.0", - "vue-router": "^3.0.2", - "vuelidate": "^0.7.5" + "vue-router": "^3.2.0", + "vuelidate": "^0.7.5", + "vuex": "^3.4.0" } } diff --git a/src/admin/components/Header.vue b/src/admin/components/Header.vue index cc03bd5..d344378 100644 --- a/src/admin/components/Header.vue +++ b/src/admin/components/Header.vue @@ -7,13 +7,22 @@ img(src="../../images/userfiles/admin.jpg").user__avatar-img .user__name span Максим Стогний - a.exit-btn(href="#") Выйти + a.exit-btn(href="#" @click.prevent="logout") Выйти .header__title Панель Администрирования .header__btn - a.exit-btn(href="#") Выйти + a.exit-btn(href="#" @click.prevent="logout") Выйти diff --git a/src/admin/components/Login.vue b/src/admin/components/Login.vue index 8d0fe85..8be0401 100644 --- a/src/admin/components/Login.vue +++ b/src/admin/components/Login.vue @@ -9,8 +9,8 @@ CustomInput( title="Логин" icon="user-empty" - v-model="user.login" - :errorText="validationMessage('login')" + v-model="user.name" + :errorText="validationMessage('name')" ) .login__row CustomInput( @@ -26,8 +26,9 @@ ).login__send-data Отправить \ No newline at end of file diff --git a/src/admin/store/modules/categories.js b/src/admin/store/modules/categories.js index f9843a1..c8c14d2 100644 --- a/src/admin/store/modules/categories.js +++ b/src/admin/store/modules/categories.js @@ -4,15 +4,117 @@ export default { namespaced: true, state: { - user: {} - }, - - getters: { + categories: [] }, mutations: { + setCategories (state, categories) { + state.categories = categories + }, + + addCategory (state, categories) { + state.categories = [categories, ...state.categories] + }, + + editCategory (state, category) { + state.categories = state.categories.map(item => item.id === category.id ? category : item) + }, + + removeCategory (state, categoryId) { + state.categories = state.categories.filter(category => category.id !== categoryId) + }, + + addSkill (state, skill) { + state.categories = state.categories.map(category => { + if (category.id === skill.category) { + const skills = category.skills || [] + category.skills = [...skills, skill] + } + + return category + }) + }, + + editSkill (state, skill) { + const category = state.categories.find(category => category.id === skill.category) + if (category) { + const skills = category.skills + category.skills = skills.map(el => el.id === skill.id ? skill : el) + } + }, + + removeSkill (state, skill) { + const category = state.categories.find(category => category.id === skill.category) + if (category) { + const skills = category.skills + category.skills = skills.filter(el => el.id !== skill.id) + } + } }, actions: { + async loadCategories ({ commit }, userId) { + try { + const { data } = await axios.get(`/categories/${userId}`) + commit('setCategories', data) + } catch (error) { + console.log(error) + } + }, + + async saveCategory ({ commit }, title) { + try { + const { data } = await axios.post('/categories', { title }) + commit('addCategory', data) + } catch (error) { + throw new Error(error) + console.log(error) + } + }, + + async updateCategory ({ commit }, category) { + try { + const { data } = await axios.post(`/categories/${category.id}`, { title: category.category }) + commit('editCategory', data.category) + } catch (error) { + console.log(error) + } + }, + + async deleteCategory ({ commit }, categoryId) { + try { + await axios.delete(`/categories/${categoryId}`) + commit('removeCategory', categoryId) + } catch (error) { + console.log(error) + } + }, + + async saveSkill ({ commit }, skill) { + try { + const { data } = await axios.post('/skills', skill); + commit('addSkill', data); + } catch (error) { + console.log(error) + } + }, + + async deleteSkill ({ commit }, skill) { + try { + await axios.delete(`/skills/${ skill.id }`); + commit('removeSkill', skill); + } catch (error) { + console.log(error) + } + }, + + async updateSkill ({ commit }, skill) { + try { + const { data } = await axios.post(`/skills/${ skill.id }`, skill); + commit('editSkill', data.skill); + } catch (error) { + console.log(error) + } + } } } \ No newline at end of file From 1505310ea6ae8311b1ba028e18f45ba5485ddef4 Mon Sep 17 00:00:00 2001 From: Maxim Stogniy Date: Fri, 22 May 2020 04:17:13 +1200 Subject: [PATCH 08/19] Review connected to backend --- src/admin/components/Reviews/Review.vue | 55 +++++++++++-- src/admin/components/Reviews/ReviewEdit.vue | 73 ++++++++++++----- src/admin/components/Reviews/Reviews.vue | 88 +++++++++++---------- src/admin/store/modules/reviews.js | 65 ++++++++++++++- 4 files changed, 210 insertions(+), 71 deletions(-) diff --git a/src/admin/components/Reviews/Review.vue b/src/admin/components/Reviews/Review.vue index 4374ec3..fc8fe21 100644 --- a/src/admin/components/Reviews/Review.vue +++ b/src/admin/components/Reviews/Review.vue @@ -3,27 +3,59 @@ .review__author .author .author__avatar - img(:src="value.photo").author__avatar-img + img(:src="reviewImage").author__avatar-img .author__data - .author__name {{value.author}} - .author__desc {{value.occ}} + .author__name {{review.author}} + .author__desc {{review.occ}} hr.divider .review__content .review__text - p {{value.text}} + p {{review.text}} .review__btns - CardBtn(title="Править" icon="edit") - CardBtn(title="Удалить" icon="delete") + CardBtn( + title="Править" + icon="edit" + @click="editReview" + ) + CardBtn( + title="Удалить" + icon="delete" + @click="deleteReview(review.id)" + ) @@ -61,6 +93,13 @@ export default { flex-shrink: 0; } + .author__avatar-img { + object-fit: cover; + object-position: center; + width: 100%; + height: 100%; + } + .author__name { font-size: 18px; font-weight: 700; diff --git a/src/admin/components/Reviews/ReviewEdit.vue b/src/admin/components/Reviews/ReviewEdit.vue index 500685b..2c9a7d8 100644 --- a/src/admin/components/Reviews/ReviewEdit.vue +++ b/src/admin/components/Reviews/ReviewEdit.vue @@ -1,6 +1,9 @@ \ No newline at end of file diff --git a/src/admin/store/index.js b/src/admin/store/index.js index f9e0f76..c5450a1 100644 --- a/src/admin/store/index.js +++ b/src/admin/store/index.js @@ -4,12 +4,14 @@ import auth from './modules/auth' import works from './modules/works' import reviews from './modules/reviews' import categories from './modules/categories' +import toast from './modules/toast' Vue.use(Vuex) export default new Vuex.Store({ modules: { auth, works, + toast, reviews, categories } diff --git a/src/admin/store/modules/categories.js b/src/admin/store/modules/categories.js index c8c14d2..7fb4659 100644 --- a/src/admin/store/modules/categories.js +++ b/src/admin/store/modules/categories.js @@ -1,4 +1,5 @@ import axios from '../../customAxios' +import generateError from '../../utils/generateError' export default { namespaced: true, @@ -58,7 +59,7 @@ export default { const { data } = await axios.get(`/categories/${userId}`) commit('setCategories', data) } catch (error) { - console.log(error) + generateError(error) } }, @@ -66,9 +67,12 @@ export default { try { const { data } = await axios.post('/categories', { title }) commit('addCategory', data) + commit('toast/showToast', + { type: 'success', message: 'Категория успешно добавлена' }, + { root: true } + ) } catch (error) { - throw new Error(error) - console.log(error) + generateError(error) } }, @@ -76,8 +80,12 @@ export default { try { const { data } = await axios.post(`/categories/${category.id}`, { title: category.category }) commit('editCategory', data.category) + commit('toast/showToast', + { type: 'success', message: 'Категория успешно обновлена' }, + { root: true } + ) } catch (error) { - console.log(error) + generateError(error) } }, @@ -85,8 +93,12 @@ export default { try { await axios.delete(`/categories/${categoryId}`) commit('removeCategory', categoryId) + commit('toast/showToast', + { type: 'success', message: 'Категория успешно удалена' }, + { root: true } + ) } catch (error) { - console.log(error) + generateError(error) } }, @@ -94,26 +106,38 @@ export default { try { const { data } = await axios.post('/skills', skill); commit('addSkill', data); + commit('toast/showToast', + { type: 'success', message: 'Скилл успешно добавлен' }, + { root: true } + ) } catch (error) { - console.log(error) + generateError(error) } }, - async deleteSkill ({ commit }, skill) { + async updateSkill ({ commit }, skill) { try { - await axios.delete(`/skills/${ skill.id }`); - commit('removeSkill', skill); + const { data } = await axios.post(`/skills/${ skill.id }`, skill); + commit('editSkill', data.skill); + commit('toast/showToast', + { type: 'success', message: 'Скилл успешно обновлен' }, + { root: true } + ) } catch (error) { - console.log(error) + generateError(error) } }, - async updateSkill ({ commit }, skill) { + async deleteSkill ({ commit }, skill) { try { - const { data } = await axios.post(`/skills/${ skill.id }`, skill); - commit('editSkill', data.skill); + await axios.delete(`/skills/${ skill.id }`); + commit('removeSkill', skill); + commit('toast/showToast', + { type: 'success', message: 'Скилл успешно удален' }, + { root: true } + ) } catch (error) { - console.log(error) + generateError(error) } } } diff --git a/src/admin/store/modules/reviews.js b/src/admin/store/modules/reviews.js index 16ac762..972aaf1 100644 --- a/src/admin/store/modules/reviews.js +++ b/src/admin/store/modules/reviews.js @@ -1,5 +1,6 @@ import axios from '../../customAxios' import formData from '../../utils/formData' +import generateError from '../../utils/generateError' export default { namespaced: true, @@ -43,21 +44,24 @@ export default { const { data } = await axios.get(`/reviews/${userId}`) commit('setReviews', data) } catch (error) { - console.log(error) + generateError(error) } }, async saveReview ({ commit }, review) { try { const { data } = await axios.post( - '/reviews', + '/reivews', formData(review), { headers: { 'Content-Type': 'multipart/form-data' } } ) commit('addReview', data) + commit('toast/showToast', + { type: 'success', message: 'Отзыв успешно добавлен' }, + { root: true } + ) } catch (error) { - throw new Error(error) - console.log(error) + generateError(error) } }, @@ -69,8 +73,12 @@ export default { { headers: { 'Content-Type': 'multipart/form-data' } } ) commit('editReview', data.review) + commit('toast/showToast', + { type: 'success', message: 'Отзыв успешно обновлен' }, + { root: true } + ) } catch (error) { - console.log(error) + generateError(error) } }, @@ -78,8 +86,12 @@ export default { try { await axios.delete(`/reviews/${reviewId}`) commit('removeReview', reviewId) + commit('toast/showToast', + { type: 'success', message: 'Отзыв успешно удален' }, + { root: true } + ) } catch (error) { - console.log(error) + generateError(error) } } } diff --git a/src/admin/store/modules/toast.js b/src/admin/store/modules/toast.js new file mode 100644 index 0000000..8a0a8e3 --- /dev/null +++ b/src/admin/store/modules/toast.js @@ -0,0 +1,24 @@ +export default { + namespaced: true, + state: { + showed: false, + message: 'Сообщение успешно отправленно', + type: 'success' + }, + mutations: { + setVisibility (state, value) { + state.showed = value + }, + setMessage (state, message) { + state.message = message + }, + setType (state, type) { + state.type = type + }, + showToast (state, { type, message }) { + state.showed = true + state.message = message + state.type = type + } + } +} \ No newline at end of file diff --git a/src/admin/store/modules/works.js b/src/admin/store/modules/works.js index 6c923f3..e6b1258 100644 --- a/src/admin/store/modules/works.js +++ b/src/admin/store/modules/works.js @@ -43,7 +43,7 @@ export default { const { data } = await axios.get(`/works/${userId}`) commit('setWorks', data) } catch (error) { - console.log(error) + generateError(error) } }, @@ -55,9 +55,12 @@ export default { { headers: { 'Content-Type': 'multipart/form-data' } } ) commit('addWork', data) + commit('toast/showToast', + { type: 'success', message: 'Работа успешно добавлена' }, + { root: true } + ) } catch (error) { - throw new Error(error) - console.log(error) + generateError(error) } }, @@ -69,8 +72,12 @@ export default { { headers: { 'Content-Type': 'multipart/form-data' } } ) commit('editWork', data.work) + commit('toast/showToast', + { type: 'success', message: 'Работа успешно обновлена' }, + { root: true } + ) } catch (error) { - console.log(error) + generateError(error) } }, @@ -78,8 +85,12 @@ export default { try { await axios.delete(`/works/${workId}`) commit('removeWork', workId) + commit('toast/showToast', + { type: 'success', message: 'Работа успешно удалена' }, + { root: true } + ) } catch (error) { - console.log(error) + generateError(error) } } } diff --git a/src/admin/utils/generateError.js b/src/admin/utils/generateError.js new file mode 100644 index 0000000..1df9440 --- /dev/null +++ b/src/admin/utils/generateError.js @@ -0,0 +1,23 @@ +export default (error) => { + const errorResponseObject = error.response.data; + + if (errorResponseObject.message) { + switch (errorResponseObject.message) { + case "The given data was invalid.": + throw new Error("Ошибка валидации данных на сервере"); + default: + throw new Error(errorResponseObject.message); + } + } + + if (errorResponseObject.error) { + switch (errorResponseObject.error) { + case "token_not_provided": + throw new Error("Токен авторизации не предоставлен"); + case "token_expired": + throw new Error("Токен авторизации просрочен"); + default: + throw new Error(errorResponseObject.error); + } + } +}; \ No newline at end of file From 55cbb4e757c9d51be8b783022bf09c03afd999d8 Mon Sep 17 00:00:00 2001 From: Maxim Stogniy Date: Sun, 24 May 2020 21:09:50 +1200 Subject: [PATCH 15/19] Change router beforeEach checking --- src/admin/router/index.js | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/admin/router/index.js b/src/admin/router/index.js index e21bcc6..c6b59bb 100644 --- a/src/admin/router/index.js +++ b/src/admin/router/index.js @@ -14,21 +14,32 @@ router.beforeEach(async (to, from, next) => { const isAuthRequired = to.matched.some(record => record.meta.auth) const isUserLogged = store.getters["auth/isLogged"] - if (isAuthRequired && !isUserLogged) { - const token = localStorage.getItem('user-token') - axios.defaults.headers['Authorization'] = `Bearer ${ token }` - - try { - const response = await axios.get('/user') - store.commit("auth/setUser", response.data.user) + if (!isUserLogged) { + const token = localStorage.getItem('user-token'); + if (!token && !isAuthRequired) { next() - } catch (e) { - await router.replace('/login') - localStorage.removeItem('user-token') + } else if (token) { + axios.defaults.headers['Authorization'] = `Bearer ${ token }` + try { + const response = await axios.get('/user') + store.commit("auth/setUser", response.data.user) + if (from.path === "/login") { + next() + } + next({ path: from.path }) + } catch (e) { + localStorage.removeItem('user-token') + next('/login') + } + } else { + next('/login') } + } else if (isUserLogged && !isAuthRequired) { + next({ path: from.path }) } else { - next() + next () } + document.title = to.meta.title || '' }) From dd4afd7e73a7ce2a9ce3c7e3a26012273c548220 Mon Sep 17 00:00:00 2001 From: Maxim Stogniy Date: Mon, 25 May 2020 00:55:56 +1200 Subject: [PATCH 16/19] Add test for Login form --- cypress.json | 1 + cypress/integration/login.js | 26 ++ package.json | 4 +- src/admin/components/Login.vue | 17 +- yarn.lock | 560 +++++++++++++++++++++++++++++++-- 5 files changed, 581 insertions(+), 27 deletions(-) create mode 100644 cypress.json create mode 100644 cypress/integration/login.js diff --git a/cypress.json b/cypress.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/cypress.json @@ -0,0 +1 @@ +{} diff --git a/cypress/integration/login.js b/cypress/integration/login.js new file mode 100644 index 0000000..78cd135 --- /dev/null +++ b/cypress/integration/login.js @@ -0,0 +1,26 @@ +beforeEach(() => { + cy.visit("http://localhost:8080/admin") +}) +it("На странице есть кнопка “Отправить”", () => { + cy.get('.login__btn').then($el => { + const text = $el.text() + cy.wrap(text).should("not.be.empty") + cy.wrap(text).should("not.contain", null) + cy.wrap(text).should("not.contain", undefined) + cy.wrap(text).should("contain", 'Отправить') + }) +}); + +it("Форма имеет все необходимые поля", () => { + cy.get('input[name="login"]') + cy.get('input[name="password"]') + cy.get('.login__btn') +}); + +it("Кнопка “Отправить” заблокирована", () => { + cy.get('.login__btn').should('have.class', 'blocked') + cy.get('input[name="login"]').type('login') + + cy.get('input[name="password"]').type('password') + cy.get('.login__btn').should('not.have.class', 'blocked') +}); \ No newline at end of file diff --git a/package.json b/package.json index 24c13e9..469af49 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "webpack-dev-server --mode=development --hot", "build": "rm -rf dist && webpack --mode=production --progress", - "reg": "node reg-util.js" + "reg": "node reg-util.js", + "cypress": "cypress open" }, "keywords": [], "author": "Maxim Stogniy ", @@ -59,6 +60,7 @@ "dependencies": { "axios": "^0.19.2", "babel-polyfill": "^6.26.0", + "cypress": "^4.6.0", "normalize.css": "^8.0.1", "promptly": "^3.0.3", "pug": "^2.0.3", diff --git a/src/admin/components/Login.vue b/src/admin/components/Login.vue index 7bbe732..c4f4e8b 100644 --- a/src/admin/components/Login.vue +++ b/src/admin/components/Login.vue @@ -7,6 +7,7 @@ .login__form-title Авторизация .login__row CustomInput( + name="login" title="Логин" icon="user-empty" v-model="user.name" @@ -14,16 +15,18 @@ ) .login__row CustomInput( + name="password" title="Пароль" icon="key" type="password" v-model="user.password" :errorText="validationMessage('password')" ) - .login__btn + .login__btns button( + :class="{ 'blocked': isBlocked }" type="submit" - ).login__send-data Отправить + ).login__btn Отправить