From 9b2bab9124a5f3a66e3f6f6796d8b8256bbf95a7 Mon Sep 17 00:00:00 2001 From: Cindy Chang Date: Thu, 27 Jun 2024 14:21:27 +0800 Subject: [PATCH 1/2] feature: admin role edit API --- src/components/Header.vue | 2 +- src/stores/acctMgmt.js | 36 +++++++++++++++++++ .../AccountAdmin/AccountAdmin.vue | 31 ++++++++++++---- 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/components/Header.vue b/src/components/Header.vue index aa14a46..362e194 100644 --- a/src/components/Header.vue +++ b/src/components/Header.vue @@ -10,7 +10,7 @@ - diff --git a/src/stores/acctMgmt.js b/src/stores/acctMgmt.js index fc64828..099eff1 100644 --- a/src/stores/acctMgmt.js +++ b/src/stores/acctMgmt.js @@ -147,6 +147,42 @@ export default defineStore('acctMgmtStore', { return false; }; }, + /** Add a role to the user in database. + * @param {string} usernameToEdit + * @param {string} roleCode + */ + async addRoleToUser(usernameToEdit, roleCode) { + const apiAddRole = `/api/users/${usernameToEdit}/roles/${roleCode}`; + + try{ + const response = await this.$axios.put(apiAddRole); + if(response.status === 200) { + return true; + } + } + catch(error) { + apiError(error, 'Failed to add role to the account.'); + return false; + }; + }, + /** Delete a role from the user in database. + * @param {string} usernameToEdit + * @param {string} roleCode + */ + async deleteRoleToUser(usernameToEdit, roleCode) { + const apiDeleteRole = `/api/users/${usernameToEdit}/roles/${roleCode}`; + + try{ + const response = await this.$axios.delete(apiDeleteRole); + if(response.status === 200) { + return true; + } + } + catch(error) { + apiError(error, 'Failed to delete a role frome the account.'); + return false; + }; + }, /** * Get user detail by unique username. * @param {string} uniqueUsername diff --git a/src/views/AccountManagement/AccountAdmin/AccountAdmin.vue b/src/views/AccountManagement/AccountAdmin/AccountAdmin.vue index 8b7b5cf..9371c9d 100644 --- a/src/views/AccountManagement/AccountAdmin/AccountAdmin.vue +++ b/src/views/AccountManagement/AccountAdmin/AccountAdmin.vue @@ -42,10 +42,10 @@
Radio On Radio Off
@@ -284,6 +284,28 @@ export default { toast.success(i18next.t("AcctMgmt.MsgAccountEdited")); } + const onAdminInputClick = async(userData, inputIsAdminOn) => { + const ADMIN_ROLE_NAME = 'admin'; + switch(inputIsAdminOn) { + case true: + await acctMgmtStore.addRoleToUser(userData.username, ADMIN_ROLE_NAME); + break; + case false: + await acctMgmtStore.deleteRoleToUser(userData.username, ADMIN_ROLE_NAME); + break; + default: + break; + } + const userDataToReplace = { + username: userData.username, + name: userData.name, + is_admin: inputIsAdminOn, + }; + + acctMgmtStore.updateSingleAccountPiniaState(userDataToReplace); + toast.success(i18next.t("AcctMgmt.MsgAccountEdited")); + } + onMounted(async () => { await fetchLoginUserData(); await acctMgmtStore.getAllUserAccounts(); @@ -339,6 +361,7 @@ export default { handleScroll, getRowClass, onDeleteBtnClick, + onAdminInputClick, handleDeleteMouseOver, handleDeleteMouseOut, handleRowMouseOver, @@ -373,10 +396,6 @@ export default { ...mapState(useAcctMgmtStore, ['allUserAccoutList']), }, methods: { - onAdminRightsBtnClick(isOn){ - - }, - /** * 無限滾動: 監聽 scroll 有沒有滾到底部 * @param {element} event 滾動傳入的事件 From a4aab21b981f54d86bc93b4eb81ce7b7af26a77a Mon Sep 17 00:00:00 2001 From: Cindy Chang Date: Fri, 28 Jun 2024 12:10:19 +0800 Subject: [PATCH 2/2] feature: refresh token. if not logged in then refresh token; else redirect to login page. --- src/router/index.js | 2 +- src/stores/login.js | 46 ++++++++++++----------- src/utils/cookieUtil.js | 38 ++++++++++--------- src/views/Login/{index.vue => Login.vue} | 0 src/views/MainContainer.vue | 48 ++++++++++++++++++------ 5 files changed, 84 insertions(+), 50 deletions(-) rename src/views/Login/{index.vue => Login.vue} (100%) diff --git a/src/router/index.js b/src/router/index.js index 6d40897..deed596 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,7 +1,7 @@ import { createRouter, createWebHistory, } from "vue-router"; import AuthContainer from '@/views/AuthContainer.vue'; import MainContainer from '@/views/MainContainer.vue'; -import Login from '@/views/Login/index.vue'; +import Login from '@/views/Login/Login.vue'; import Files from '@/views/Files/index.vue'; import Upload from '@/views/Upload/index.vue'; import Map from '@/views/Discover/Map/index.vue'; diff --git a/src/stores/login.js b/src/stores/login.js index 674c635..a60f201 100644 --- a/src/stores/login.js +++ b/src/stores/login.js @@ -1,7 +1,7 @@ import { defineStore } from "pinia"; import axios from 'axios'; import apiError from '@/module/apiError.js'; -import { deleteCookie, setCookie } from "../utils/cookieUtil"; +import { deleteCookie, setCookie, getCookie } from "../utils/cookieUtil"; export default defineStore('loginStore', { // data, methods, computed @@ -11,13 +11,13 @@ export default defineStore('loginStore', { grant_type: 'password', // password | refresh_token username: '', password: '', - refresh_token: '' + refresh_token: undefined, }, isInvalid: false, userData: {}, isLoggedIn: false, rememberedReturnToUrl: "", - // expired: new Date().setMonth(6), // 設定 Refresh Token 的到期日為半年後 + expired: new Date().setMonth(6), // 設定 Refresh Token 的到期日為半年後 }), actions: { /** @@ -35,10 +35,10 @@ export default defineStore('loginStore', { try { const response = await axios.post(api, this.auth, config); const accessToken = response.data.access_token; - const refreshToken = response.data.refresh_token; + const refresh_token = response.data.refresh_token; // 將 token 儲存在 cookie document.cookie = `luciaToken=${accessToken}`; - // document.cookie = `luciaRefreshToken=${refreshToken};expires=${new Date(this.expired)};`; + document.cookie = `luciaRefreshToken=${refresh_token};expires=${new Date(this.expired)};`; this.isLoggedIn = true; setCookie("isLuciaLoggedIn", "true"); @@ -57,27 +57,31 @@ export default defineStore('loginStore', { }; }, /** - * Refresh Token (暫時沒做) + * Refresh Token */ - async refreshTokenLogin() { + async refreshToken() { + console.log('TODO:TODO:', this.auth); const api = '/api/oauth/token'; - const refreshToken = document.cookie.replace(/(?:(?:^|.*;\s*)luciaRefreshToken\s*\=\s*([^;]*).*$)|^.*$/, "$1"); this.auth.grant_type = 'refresh_token'; - this.auth.refresh_token = refreshToken; + this.auth.refresh_token = getCookie("luciaRefreshToken"); - // try { - // const response = await axios.post(api, this.auth, config); - // const newAccessToken = response.data.access_token; - // const newRefreshToken = response.data.refresh_token; - - // document.cookie = `luciaToken=${newAccessToken}`; - // document.cookie = `luciaRefreshToken=${newRefreshToken};expires=${this.expired}`; - - // defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`; - // } catch(error) { - // this.$router.push('/login'); - // } + try { + const response = await axios.post(api, this.auth, config); + console.log('response', response); + if(response.status === 200) { + const newAccessToken = response.data.access_token; + const newRefreshToken = response.data.refresh_token; + + document.cookie = `luciaToken=${newAccessToken}`; + document.cookie = `luciaRefreshToken=${newRefreshToken};expires=${this.expired}`; + + defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`; + } + } catch(error) { + // 若refresh token 失敗則導向至登入頁面 + this.$router.push('/login'); + } }, /** * Logout, tooken expired diff --git a/src/utils/cookieUtil.js b/src/utils/cookieUtil.js index c13ca71..5be2e8b 100644 --- a/src/utils/cookieUtil.js +++ b/src/utils/cookieUtil.js @@ -1,29 +1,33 @@ export function getCookie(name) { - const nameEQ = name + "="; - const ca = document.cookie.split(';'); - for (let i = 0; i < ca.length; i++) { - let c = ca[i]; + const nameEqual = name + "="; + const cookieArr = document.cookie.split(';'); + for (let i = 0; i < cookieArr.length; i++) { + let c = cookieArr[i]; while (c.charAt(0) === ' ') { c = c.substring(1, c.length); } - if (c.indexOf(nameEQ) === 0) { - return c.substring(nameEQ.length, c.length); + if (c.indexOf(nameEqual) === 0) { + return c.substring(nameEqual.length, c.length); } } return null; } - export function setCookie(name, value, days=1) { - let expires = ""; - if (days) { - const date = new Date(); - date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); - expires = "; expires=" + date.toUTCString(); - } - document.cookie = name + "=" + (value || "") + expires + "; path=/"; +export function setCookie(name, value, days=1) { + let expires = ""; + if (days) { + const date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = "; expires=" + date.toUTCString(); } + document.cookie = name + "=" + (value || "") + expires + "; path=/"; +} - export function deleteCookie(name, path = '/') { - document.cookie = name + '=; Max-Age=-99999999; path=' + path; - } +export function setCookieWithoutExpiration(name, value) { + document.cookie = name + "=" + (value || ""); +} + +export function deleteCookie(name, path = '/') { + document.cookie = name + '=; Max-Age=-99999999; path=' + path; +} \ No newline at end of file diff --git a/src/views/Login/index.vue b/src/views/Login/Login.vue similarity index 100% rename from src/views/Login/index.vue rename to src/views/Login/Login.vue diff --git a/src/views/MainContainer.vue b/src/views/MainContainer.vue index fe30e28..bd48115 100644 --- a/src/views/MainContainer.vue +++ b/src/views/MainContainer.vue @@ -33,9 +33,11 @@ export default { const allMapDataStore = AllMapDataStore(); const conformanceStore = ConformanceStore(); const pageAdminStore = PageAdminStore(); + const loginStore = LoginStore(); const { tempFilterId, createFilterId, temporaryData, postRuleData, ruleData } = storeToRefs(allMapDataStore); const { conformanceLogTempCheckId, conformanceFilterTempCheckId } = storeToRefs(conformanceStore); const router = useRouter(); + const { isLoggedIn, auth } = storeToRefs(loginStore); const setHighlightedNavItemOnLanding = () => { const currentPath = router.currentRoute.value.path; @@ -72,6 +74,7 @@ export default { ]), ...mapState(LoginStore, [ 'isLoggedIn', + 'auth', ]) }, methods: { @@ -81,23 +84,46 @@ export default { 'clearShouldKeepPreviousPageBoolean', 'setActivePageComputedByRoute', ],), + ...mapActions(LoginStore, [ + 'refreshToken', + ],), }, created() { // Save token in Headers. const token = document.cookie.replace(/(?:(?:^|.*;\s*)luciaToken\s*\=\s*([^;]*).*$)|^.*$/, "$1"); this.$http.defaults.headers.common['Authorization'] = `Bearer ${token}`; }, - beforeRouteEnter(to, from, next) { - // 重新整理畫面以及第一次進入網頁時,beforeRouteEnter這個hook會被執行,然而beforeRouteUpdate不會被執行 - if (!getCookie("isLuciaLoggedIn")) { - next({ - path: '/login', - query: { - 'return-to': btoa(window.location.href), - } - }); - } else { - next(); + // 重新整理畫面以及第一次進入網頁時,beforeRouteEnter這個hook會被執行,然而beforeRouteUpdate不會被執行 + // PSEUDOCODE + // if (not logged in) { + // if (has refresh token) { + // refresh_token(); + // if (refresh failed) { + // go to log in(); + // } else { + // cookie add("refresh_token=" + refresh_token "; expire=****") + // } + // } else { + // go to log in(); + // } + // } + beforeRouteEnter(to, from, next) { + const loginStore = LoginStore(); + + if (!loginStore.isLoggedIn) { + if (getCookie('luciaRefreshToken')) { + loginStore.refreshToken(); + } else { + next({ + path: '/login', + query: { + // 記憶未來登入後要進入的網址,且記憶的時候要用base64編碼包裹住 + 'return-to': btoa(window.location.href), + } + }); + } + } else { + next(); } }, // Remember, Swal modal handling is called before beforeRouteUpdate