// The Lucia project. // Copyright 2023-2026 DSP, inc. All rights reserved. // Authors: // chiayin.kuo@dsp.im (chiayin), 2023/1/31 // imacat.yang@dsp.im (imacat), 2023/9/23 // cindy.chang@dsp.im (Cindy Chang), 2024/5/30 /** * @module stores/acctMgmt Account management store for CRUD * operations on user accounts (admin functionality). */ import { defineStore } from 'pinia'; import apiClient from '@/api/client.js'; import apiError from '@/module/apiError'; import { useLoginStore } from '@/stores/login'; import { JUST_CREATE_ACCOUNT_HOT_DURATION_MINS } from '@/constants/constants'; interface User { username: string; detail: Record; name?: string; is_admin?: boolean; is_sso?: boolean; isCurrentLoggedIn: boolean, isDeleteHovered?: boolean; isRowHovered?: boolean; isEditHovered?: boolean; isDetailHovered?: boolean; } interface EditDetail { newUsername?: string; username?: string; password: string; name: string; is_active: boolean; } export const useAcctMgmtStore = defineStore('acctMgmtStore', { persist: true, state: () => ({ allUserAccoutList: [] as User[], isAcctMenuOpen: false, currentViewingUser: { username: '', detail: {}, } as User, response: { deleteAccount: null, }, isOneAccountJustCreate: false, //如果有一個帳號剛剛建立,則會在列表上的第一列顯示這個帳號,並且右置一個徽章 justCreateUsername: '', // unique username shouldUpdateList: false, // 控制是否該刷新列表 }), getters: {}, actions: { /** * Set related boolean to true */ openAcctMenu() { this.isAcctMenuOpen = true; }, /** * Set related boolean to false */ closeAcctMenu() { this.isAcctMenuOpen = false; }, toggleIsAcctMenuOpen() { this.isAcctMenuOpen = !this.isAcctMenuOpen; }, /** * For convenience, set current viewing data according to unique user ID. * @param {string} username */ setCurrentViewingUser(username: string) { const userFind = this.allUserAccoutList.find(user => user.username === username); this.currentViewingUser = userFind; }, /** * We have this method because we want to handle create new modal case. */ clearCurrentViewingUser() { this.currentViewingUser = { username: '', detail: {}, name: '', is_admin: false, is_sso: false, isDeleteHovered: false, isRowHovered: false, isEditHovered: false, isDetailHovered: false, }; }, /** * Get all user accounts */ async getAllUserAccounts() { const apiGetUserList = `/api/users`; try { const response = await apiClient.get(apiGetUserList); const customizedResponseData = await this.customizeAllUserList(response.data); this.allUserAccoutList = await this.moveCurrentLoginUserToFirstRow(customizedResponseData); } catch (error) { apiError(error, 'Failed to get all users.'); } }, /** * Add some customization. For example, add isHovered field */ async customizeAllUserList(rawResponseData: User[]): Promise { const loginStore = useLoginStore(); await loginStore.getUserData(); const loginUserData:User = loginStore.userData; return rawResponseData.map(user => ({ ...user, // 保留後端傳來的欄位 isCurrentLoggedIn: loginUserData.username === user.username, isDeleteHovered: false, // 針對前端顯示而額外增加的欄位 isRowHovered: false, isEditHovered: false, })); }, /** * Current logged in user should be placed at the first row on list */ async moveCurrentLoginUserToFirstRow(fetchedUserList: User[]): Promise { const loginStore = useLoginStore(); await loginStore.getUserData(); const loginUserData:User = loginStore.userData; const foundLoginUserIndex = fetchedUserList.findIndex(user => user.username === loginUserData.username); fetchedUserList.unshift(fetchedUserList.splice(foundLoginUserIndex, 1)[0]); return fetchedUserList; }, /** * Create new user in database. * @param {object} userToCreate * userToCreate { * "username": "string", * "password": "string", * "name": "string", * "is_admin": boolean, * "is_active": boolean, * } */ async createNewAccount(userToCreate: User) { const apiCreateAccount = `/api/users`; try { const response = await apiClient.post(apiCreateAccount, userToCreate); if (response.status === 200) { this.isOneAccountJustCreate = true; this.justCreateUsername = userToCreate.username; setTimeout(this.resetJustCreateFlag, JUST_CREATE_ACCOUNT_HOT_DURATION_MINS * 1000 * 60); } } catch (error) { apiError(error, 'Failed to add a new account.'); } }, /** * Delete an account from database. * @param {string} userToDelete this value is unique in database. * @returns the result of whether the deletion is success or not. */ async deleteAccount(userToDelete: string): Promise { const apiDelete = `/api/users/${userToDelete}`; try { const response = await apiClient.delete(apiDelete); return response.status === 200; } catch (error) { apiError(error, 'Failed to delete the account.'); return false; } }, /** * Edit an account. * @param {string} userToEdit this value is unique in database. * @param {object} editDetail */ async editAccount(userToEdit: string, editDetail: EditDetail): Promise { const apiEdit = `/api/users/${userToEdit}`; try { const response = await apiClient.put(apiEdit, { username: editDetail.newUsername ? editDetail.newUsername : editDetail.username, password: editDetail.password, name: editDetail.name, is_active: editDetail.is_active, }); return response.status === 200; } catch (error) { apiError(error, 'Failed to edit the account.'); return false; } }, async editAccountName(userToEdit: string, newName: string): Promise { const apiEdit = `/api/users/${userToEdit}`; try { const response = await apiClient.put(apiEdit, { username: userToEdit, name: newName, is_active: this.currentViewingUser.is_active, is_admin: this.currentViewingUser.is_admin, }); return response.status === 200; } catch (error) { apiError(error, 'Failed to edit name of account.'); return false; } }, async editAccountPwd(userToEdit: string, newPwd: string): Promise { const apiEdit = `/api/users/${userToEdit}`; try { const response = await apiClient.put(apiEdit, { username: userToEdit, name: this.currentViewingUser.name, password: newPwd, is_active: this.currentViewingUser.is_active, is_admin: this.currentViewingUser.is_admin, }); return response.status === 200; } catch (error) { apiError(error, 'Failed to edit password of account.'); return false; } }, /** Add a role to the user in database. * @param {string} usernameToEdit * @param {string} roleCode */ async addRoleToUser(usernameToEdit: string, roleCode: string): Promise { const apiAddRole = `/api/users/${usernameToEdit}/roles/${roleCode}`; try { const response = await apiClient.put(apiAddRole); return response.status === 200; } 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: string, roleCode: string): Promise { const apiDeleteRole = `/api/users/${usernameToEdit}/roles/${roleCode}`; try { const response = await apiClient.delete(apiDeleteRole); return response.status === 200; } catch (error) { apiError(error, 'Failed to delete a role from the account.'); return false; } }, /** * Get user detail by unique username. * @param {string} uniqueUsername */ async getUserDetail(uniqueUsername: string): Promise { const apiUserDetail = `/api/users/${uniqueUsername}`; try { const response = await apiClient.get(apiUserDetail); this.currentViewingUser = response.data; this.currentViewingUser.is_admin = response.data.roles.some(role => role.code === 'admin'); return response.status === 200; } catch (error) { //不需要跳出錯誤,因為如果是錯誤反而是好事,表示帳號是獨一的 return false; } }, /** * According to mouseover or mouseleave status, change the bool value. * @param {string} username * @param {boolean} isDeleteHovered */ changeIsDeleteHoveredByUser(username: string, isDeleteHovered: boolean) { const userToChange = this.allUserAccoutList.find(user => user.username === username); if (userToChange) { userToChange.isDeleteHovered = isDeleteHovered; } }, /** * According to mouseover or mouseleave status, change the bool value. * @param {string} username * @param {boolean} isRowHovered */ changeIsRowHoveredByUser(username: string, isRowHovered: boolean) { const userToChange = this.allUserAccoutList.find(user => user.username === username); if (userToChange) { userToChange.isRowHovered = isRowHovered; } }, /** * According to mouseover or mouseleave status, change the bool value. * @param {string} username * @param {boolean} isEditHovered */ changeIsEditHoveredByUser(username: string, isEditHovered: boolean) { const userToChange = this.allUserAccoutList.find(user => user.username === username); if (userToChange) { userToChange.isEditHovered = isEditHovered; } }, /** * According to mouseover or mouseleave status, change the bool value. * @param {string} username * @param {boolean} isEditHovered */ changeIsDetailHoveredByUser(username: string, isDetailHovered: boolean) { const userToChange = this.allUserAccoutList.find(user => user.username === username); if (userToChange) { userToChange.isDetailHovered = isDetailHovered; } }, /** * Reset isOneAccountJustCreate to false, causing the badge to disappear. */ resetJustCreateFlag() { this.isOneAccountJustCreate = false; }, /** Set the value of shouldUpdateList variable. * @param {boolean} shouldUpdateBoolean */ setShouldUpdateList(shouldUpdateBoolean: boolean) { this.shouldUpdateList = shouldUpdateBoolean; }, /** Only update one single account in the pinia state. * @param {object} userDataToReplace */ updateSingleAccountPiniaState(userDataToReplace: User) { const userIndex = this.allUserAccoutList.findIndex(user => user.username === userDataToReplace.username); if (userIndex !== -1) { this.allUserAccoutList[userIndex] = { ...this.allUserAccoutList[userIndex], ...userDataToReplace }; } } }, });