Files
lucia-frontend/src/stores/acctMgmt.ts
2026-03-09 14:10:24 +08:00

344 lines
12 KiB
TypeScript

// 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<string, any>;
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', {
state: () => ({
allUserAccountList: [] as User[],
isAcctMenuOpen: false,
currentViewingUser: {
username: '',
detail: {},
} as User,
response: {
deleteAccount: null,
},
isOneAccountJustCreate: false, // If an account was just created, display it at the top of the list with a badge
justCreateUsername: '', // unique username
shouldUpdateList: false, // Controls whether the list should be refreshed
}),
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.allUserAccountList.find(user => user.username === username);
if (!userFind) return;
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.allUserAccountList = 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<User[]> {
const loginStore = useLoginStore();
await loginStore.getUserData();
const loginUserData:User = loginStore.userData;
return rawResponseData.map(user => ({
...user, // Preserve fields from the backend response
isCurrentLoggedIn: loginUserData.username === user.username,
isDeleteHovered: false, // Additional fields for frontend display state
isRowHovered: false,
isEditHovered: false,
}));
},
/**
* Current logged in user should be placed at the first row on list
*/
async moveCurrentLoginUserToFirstRow(fetchedUserList: User[]): Promise<User[]> {
const loginStore = useLoginStore();
const loginUserData:User = loginStore.userData;
const foundLoginUserIndex = fetchedUserList.findIndex(user => user.username === loginUserData.username);
if (foundLoginUserIndex !== -1) {
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<boolean> {
const apiDelete = `/api/users/${encodeURIComponent(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<boolean> {
const apiEdit = `/api/users/${encodeURIComponent(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<boolean> {
const apiEdit = `/api/users/${encodeURIComponent(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<boolean> {
const apiEdit = `/api/users/${encodeURIComponent(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<boolean> {
const apiAddRole = `/api/users/${encodeURIComponent(usernameToEdit)}/roles/${encodeURIComponent(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<boolean> {
const apiDeleteRole = `/api/users/${encodeURIComponent(usernameToEdit)}/roles/${encodeURIComponent(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<boolean> {
const apiUserDetail = `/api/users/${encodeURIComponent(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) {
// No need to show an error, because an error here means the account is unique
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.allUserAccountList.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.allUserAccountList.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.allUserAccountList.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.allUserAccountList.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.allUserAccountList.findIndex(user => user.username === userDataToReplace.username);
if (userIndex !== -1) {
this.allUserAccountList[userIndex] = { ...this.allUserAccountList[userIndex], ...userDataToReplace };
}
}
},
});