Merge branch 'acct_mgmt' of github.com:dspim/lucia-frontend into acct_mgmt
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
<button id="logout_btn" class="btn btn-sm btn-neutral mr-2" @click.prevent="logOutButton">
|
||||
Logout
|
||||
</button>
|
||||
<img v-show="false" id="acct_mgmt_button" src="@/assets/icon-head-black.svg" width="32" height="32"
|
||||
<img v-show="true" id="acct_mgmt_button" src="@/assets/icon-head-black.svg" width="32" height="32"
|
||||
class="cursor-pointer" @click="onAcctHeadClick"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
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}`;
|
||||
document.cookie = `luciaToken=${newAccessToken}`;
|
||||
document.cookie = `luciaRefreshToken=${newRefreshToken};expires=${this.expired}`;
|
||||
|
||||
// defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`;
|
||||
// } catch(error) {
|
||||
// this.$router.push('/login');
|
||||
// }
|
||||
defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`;
|
||||
}
|
||||
} catch(error) {
|
||||
// 若refresh token 失敗則導向至登入頁面
|
||||
this.$router.push('/login');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Logout, tooken expired
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
<div class="row-container flex-w-full-hoverable flex w-full justify-center" @mouseenter="handleRowMouseOver(slotProps.data.username)"
|
||||
@mouseout="handleRowMouseOut(slotProps.data.username)">
|
||||
<img v-if="slotProps.data.is_admin" src="@/assets/radioOn.svg" alt="Radio On" class="cursor-pointer flex justify-center"
|
||||
@click="onAdminRightsBtnClick(true)"
|
||||
@click="onAdminInputClick(slotProps.data, false)"
|
||||
/>
|
||||
<img v-else src="@/assets/radioOff.svg" alt="Radio Off" class="cursor-pointer flex justify-center"
|
||||
@click="onAdminRightsBtnClick(false)"
|
||||
@click="onAdminInputClick(slotProps.data, true)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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 滾動傳入的事件
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
<template>
|
||||
<div class="container h-screen-main">
|
||||
<div class="flex justify-center items-center min-h-full">
|
||||
<div class="w-full max-w-md p-8">
|
||||
<h2 class="text-center text-2xl mb-4 font-semibold">LOGIN</h2>
|
||||
<form action="#" method="post" class="w-full grid gap-2" @submit.prevent="signIn">
|
||||
<label for="account" class="relative">
|
||||
<p class="text-sm font-normal mb-2">Account</p>
|
||||
<span class="absolute flex bottom-1 left-0 items-center pl-2 pr-2 border-r border-neutral-300 h-[26px]">
|
||||
<IconMember class="h-5 w-5 fill-current"/>
|
||||
</span>
|
||||
<input type="text" id="account" class="w-full border border-neutral-300 rounded py-1 pl-10 pr-2 focus:outline-none focus:border-primary focus:ring-1 "
|
||||
:class="{'border-danger':isInvalid}" required autofocus v-model.trim="auth.username"
|
||||
@change="changeHandler($event)"/>
|
||||
</label>
|
||||
<label for="passwordt" class="relative block">
|
||||
<p class="text-sm font-normal mb-2">Password</p>
|
||||
<span class="absolute flex bottom-1 left-0 items-center pl-2 pr-2 border-r border-neutral-300 h-[26px]">
|
||||
<IconLockKey class="h-5 w-5 fill-current"/>
|
||||
</span>
|
||||
<input :type="showPassword ? 'text' : 'password'" id="password" aria-describedby="password-addon"
|
||||
class="w-full border border-neutral-300 rounded py-1 pl-10 pr-2 focus:outline-none focus:border-primary focus:ring-1 "
|
||||
:class="{'border-danger':isInvalid}" required v-model.trim="auth.password" @change="changeHandler($event)"/>
|
||||
<span class="absolute bottom-2 right-4 inline-flex items-center cursor-pointer" v-show="auth.password"
|
||||
@click="showPassword = !showPassword">
|
||||
<IconEyeOpen class="h-5 w-5" v-if="showPassword"/>
|
||||
<IconEyeClose class="h-5 w-5" v-else/>
|
||||
</span>
|
||||
</label>
|
||||
<p class="my-4 text-danger">
|
||||
<span v-show="isInvalid">
|
||||
<IconWarnTriangle class="h-4 w-5 mx-2 mb-[2px] inline-block text-danger"/>
|
||||
Incorrect account or password.
|
||||
</span>
|
||||
</p>
|
||||
<button id="login_btn_main_btn" type="submit" class="w-full btn btn-lg"
|
||||
:class="this.isDisabled ? 'btn-disable' : 'btn-c-primary'"
|
||||
:disabled="isDisabledButton">
|
||||
Log in
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { storeToRefs, mapActions } from 'pinia';
|
||||
import loginStore from '@/stores/login.js';
|
||||
import IconMember from '@/components/icons/IconMember.vue';
|
||||
import IconLockKey from '@/components/icons/IconLockKey.vue';
|
||||
import IconEyeOpen from '@/components/icons/IconEyeOpen.vue';
|
||||
import IconEyeClose from '@/components/icons/IconEyeClose.vue';
|
||||
import IconWarnTriangle from '@/components/icons/IconWarnTriangle.vue';
|
||||
|
||||
export default {
|
||||
data(){
|
||||
return {
|
||||
isDisabled: true,
|
||||
showPassword: false,
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
// 調用函數,獲取 Store
|
||||
const store = loginStore();
|
||||
// 調用 store 裡的 state
|
||||
const { auth, isInvalid } = storeToRefs(store);
|
||||
// 調用 store 裡的 action
|
||||
const { signIn } = store;
|
||||
|
||||
return {
|
||||
auth,
|
||||
isInvalid,
|
||||
signIn,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
IconMember,
|
||||
IconLockKey,
|
||||
IconEyeOpen,
|
||||
IconEyeClose,
|
||||
IconWarnTriangle
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* if input no value , disabled.
|
||||
*/
|
||||
isDisabledButton() {
|
||||
let inputAreSpaces = this.auth.username === '' || this.auth.password === '' || this.isInvalid === true;
|
||||
return this.isDisabled = inputAreSpaces ? true : false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* when input onChange value , isInvalid === false.
|
||||
* @param {event} event input 傳入的事件
|
||||
*/
|
||||
changeHandler(event) {
|
||||
let inputValue = event.target.value;
|
||||
if(inputValue !== '') {
|
||||
this.isInvalid = false;
|
||||
}
|
||||
},
|
||||
...mapActions(loginStore, ['setRememberedReturnToUrl']),
|
||||
},
|
||||
created() {
|
||||
// 考慮到使用者可能在未登入的情況下貼入一個頁面網址連結過來瀏覽器
|
||||
// btoa: 對字串進行 Base64 編碼
|
||||
if(this.$route.query['return-to']) {
|
||||
this.setRememberedReturnToUrl(this.$route.query['return-to']);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Hide the eyes from a password input in MS Edge */
|
||||
input::-ms-reveal,
|
||||
input::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
/* Hide the keys from the inputs in safair */
|
||||
input::-webkit-contacts-auto-fill-button,
|
||||
input::-webkit-credentials-auto-fill-button {
|
||||
visibility: hidden;
|
||||
display: none !important;
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
width: 0;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -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,21 +84,44 @@ 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這個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) {
|
||||
// 重新整理畫面以及第一次進入網頁時,beforeRouteEnter這個hook會被執行,然而beforeRouteUpdate不會被執行
|
||||
if (!getCookie("isLuciaLoggedIn")) {
|
||||
next({
|
||||
path: '/login',
|
||||
query: {
|
||||
'return-to': btoa(window.location.href),
|
||||
}
|
||||
});
|
||||
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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user