Merge pull request #71 from Learnware-LAMDA/front_super_admin

feat(frontend): add super role in admin page
This commit is contained in:
AnnyTerfect 2023-10-31 13:41:10 +08:00 committed by GitHub
commit 01e19b4930
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 139 additions and 22 deletions

View File

@ -12,6 +12,7 @@ export interface Props {
pageNum: number;
loading?: boolean;
showPagination?: boolean;
enableSetRole?: boolean;
cols?: number;
md?: number;
sm?: number;
@ -20,12 +21,19 @@ export interface Props {
const display = useDisplay();
const emits = defineEmits(["click:reset", "click:delete", "click:export", "pageChange"]);
const emits = defineEmits([
"click:reset",
"click:delete",
"click:export",
"click:setRole",
"pageChange",
]);
const props = withDefaults(defineProps<Props>(), {
pageSize: 10,
loading: false,
showPagination: true,
enableSetRole: false,
cols: 2,
md: 1,
sm: 1,
@ -78,6 +86,10 @@ function handleClickDelete(id: string): void {
function handleClickExport(id: string): void {
emits("click:export", id);
}
function handleClickSetRole(id: string, role: number): void {
emits("click:setRole", id, role);
}
</script>
<template>
@ -89,9 +101,11 @@ function handleClickExport(id: string): void {
:md="md"
:sm="sm"
:xs="xs"
:enable-set-role="enableSetRole"
@click:delete="handleClickDelete"
@click:reset="handleClickReset"
@click:export="handleClickExport"
@click:set-role="handleClickSetRole"
/>
<div

View File

@ -5,7 +5,7 @@ import { useI18n } from "vue-i18n";
import oopsImg from "/oops.svg?url";
import type { User } from "@beiming-system/types/user";
const emits = defineEmits(["click:reset", "click:delete", "click:export"]);
const emits = defineEmits(["click:reset", "click:delete", "click:export", "click:setRole"]);
const display = useDisplay();
@ -17,6 +17,7 @@ export interface Props {
md?: number;
sm?: number;
xs?: number;
enableSetRole?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
@ -50,6 +51,10 @@ function handleClickDelete(id: number): void {
function handleClickExport(): void {
emits("click:export");
}
function handleClickSetRole(id: number, role: number): void {
emits("click:setRole", id, role);
}
</script>
<template>
@ -74,6 +79,9 @@ function handleClickExport(): void {
<div class="title">
{{ t("AllUser.Unverified") }}
</div>
<div class="title">
{{ t("AllUser.IsAdmin") }}
</div>
</div>
<v-card-actions class="actions">
<v-btn class="opacity-0" icon="mdi-lock-reset" disabled=""></v-btn>
@ -99,6 +107,16 @@ function handleClickExport(): void {
<div class="title">
<span class="small-title">Unverified: </span>{{ item.unverified_learnware_count }}
</div>
<div class="title">
<v-card-actions class="actions">
<v-checkbox
:model-value="item.role >= 1"
:disabled="!enableSetRole || item.email === 'admin@localhost'"
class="title"
@click="handleClickSetRole(item.id, item.role == 1 ? 0 : 1)"
></v-checkbox>
</v-card-actions>
</div>
</div>
<v-card-actions class="actions">
<v-btn icon="mdi-lock-reset" @click.stop="() => handleClickReset(item.id)"></v-btn>
@ -137,7 +155,7 @@ function handleClickExport(): void {
@apply flex items-center;
.columns {
@apply grid sm: "grid-cols-[3fr,3fr,1fr,1fr]" w-1/1;
@apply grid sm: "grid-cols-[3fr,3fr,1fr,1fr,1fr]" w-1/1;
.title {
@apply xl: text-1rem lg:text-lg text-xs sm: (flex flex-col items-start justify-center);

View File

@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable no-unused-vars */
/*
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
* Digest Algorithm, as defined in RFC 1321.
@ -8,7 +9,7 @@
* See http://pajhome.org.uk/crypt/md5 for more info.
*/
/*
* Configurable letiables. You may need to tweak these to be compatible with
* Configurable variables. You may need to tweak these to be compatible with
* the server-side, but the defaults work in most cases.
*/
const hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
@ -17,13 +18,13 @@ const chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */
* These are the functions you'll usually want to call
* They take string arguments and return either hex or base-64 encoded strings
*/
function hex_md5(s) {
function hex_md5(s: string) {
return binl2hex(core_md5(str2binl(s), s.length * chrsz));
}
/*
* Calculate the MD5 of an array of little-endian words, and a bit length
*/
function core_md5(x, len) {
function core_md5(x: number[], len: number) {
/* append padding */
x[len >> 5] |= 0x80 << len % 32;
x[(((len + 64) >>> 9) << 4) + 14] = len;
@ -110,26 +111,26 @@ function core_md5(x, len) {
/*
* These functions implement the four basic operations the algorithm uses.
*/
function md5_cmn(q, a, b, x, s, t) {
function md5_cmn(q: number, a: number, b: number, x: number, s: number, t: number) {
return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b);
}
function md5_ff(a, b, c, d, x, s, t) {
function md5_ff(a: number, b: number, c: number, d: number, x: number, s: number, t: number) {
return md5_cmn((b & c) | (~b & d), a, b, x, s, t);
}
function md5_gg(a, b, c, d, x, s, t) {
function md5_gg(a: number, b: number, c: number, d: number, x: number, s: number, t: number) {
return md5_cmn((b & d) | (c & ~d), a, b, x, s, t);
}
function md5_hh(a, b, c, d, x, s, t) {
function md5_hh(a: number, b: number, c: number, d: number, x: number, s: number, t: number) {
return md5_cmn(b ^ c ^ d, a, b, x, s, t);
}
function md5_ii(a, b, c, d, x, s, t) {
function md5_ii(a: number, b: number, c: number, d: number, x: number, s: number, t: number) {
return md5_cmn(c ^ (b | ~d), a, b, x, s, t);
}
/*
* Add integers, wrapping at 2^32. This uses 16-bit operations internally
* to work around bugs in some JS interpreters.
*/
function safe_add(x, y) {
function safe_add(x: number, y: number) {
const lsw = (x & 0xffff) + (y & 0xffff);
const msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xffff);
@ -137,24 +138,21 @@ function safe_add(x, y) {
/*
* Bitwise rotate a 32-bit number to the left.
*/
function bit_rol(num, cnt) {
function bit_rol(num: number, cnt: number) {
return (num << cnt) | (num >>> (32 - cnt));
}
/*
* Convert a string to an array of little-endian words
* If chrsz is ASCII, characters >255 have their hi-byte silently ignored.
*/
function str2binl(str) {
const bin = [];
function str2binl(str: string) {
const bin: number[] = [];
const mask = (1 << chrsz) - 1;
for (let i = 0; i < str.length * chrsz; i += chrsz)
bin[i >> 5] |= (str.charCodeAt(i / chrsz) & mask) << i % 32;
return bin;
}
/*
* Convert an array of little-endian words to a hex string.
*/
function binl2hex(binarray) {
function binl2hex(binarray: number[]) {
const hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
let str = "";
for (let i = 0; i < binarray.length * 4; i++) {

View File

@ -1,3 +1,4 @@
export { downloadLearnware } from "./download.js";
export { fetchex } from "./fetchex.js";
export { default as saveContentToFile } from "./saveContentToFile.js";
export { hex_md5 } from "./encrypt.js";

View File

@ -3,7 +3,7 @@ import { ref, computed } from "vue";
import { useStore } from "vuex";
import { useRouter } from "vue-router";
import { useField } from "@beiming-system/hooks";
import { fetchex } from "../utils";
import { fetchex, hex_md5 } from "../utils";
import collaborationImg from "@/assets/images/collaboration.svg?url";
const store = useStore();
@ -59,7 +59,7 @@ function login(): Promise<void> {
const data = {
email: email.value,
password: password.value,
password: hex_md5(password.value),
};
return fetchex("/api/auth/login", {
@ -110,7 +110,7 @@ function login(): Promise<void> {
}
})
.then((res) => {
if (res.data && res.data.role === 1) {
if (res.data && res.data.role >= 1) {
success.value = true;
setTimeout(() => {
store.dispatch("login");

View File

@ -28,6 +28,11 @@ const resetName = ref<string>("");
const newPasswordDialog = ref<InstanceType<typeof SuccessDialog>>(null);
const newPassword = ref("");
const setRoleDialog = ref<InstanceType<typeof ConfirmDialog>>(null);
const setRoleId = ref<number>(-1);
const setRoleRole = ref<number>(0);
const setRoleName = ref<string>("");
const showError = ref(false);
const errorMsg = ref("");
const errorTimer = ref<number>();
@ -216,6 +221,57 @@ function handleClickDelete(id: number): void {
userName && (deleteName.value = userName);
}
function handleClickSetRole(id: number, role: number): void {
setRoleDialog.value.confirm();
setRoleId.value = Number(id);
setRoleRole.value = Number(role);
const userName = userItems.value.find((item) => item.id === id)?.username;
userName && (setRoleName.value = userName);
fetchByFilterAndPage(filters.value, page.value);
}
function setRole(id: number, role: number): Promise<void> {
return fetchex("/api/admin/set_user_role", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: id,
role: role,
}),
})
.then((res) => {
if (res && res.status === 200) {
return res;
}
throw new Error("Network error");
})
.then((res) => res.json())
.then((res: { code: number; msg: string }) => {
if (res.code === 0) {
return res;
}
throw new Error(res.msg);
})
.then(() => {
store.commit("setShowGlobalError", true);
store.commit("setGlobalErrorMsg", "set successfully.");
fetchByFilterAndPage(filters.value, page.value);
})
.catch((err) => {
console.error(err);
showError.value = true;
errorMsg.value = err.message;
clearTimeout(errorTimer.value);
errorTimer.value = Number(
setTimeout(() => {
showError.value = false;
}, 3000),
);
});
}
async function handleClickExport(): Promise<void> {
const table = [["Username", "Email", "Verified", "Unverified"]];
for (let _page = 1; _page <= pageNum.value; _page++) {
@ -340,6 +396,20 @@ onActivated(() => {
</template>
</confirm-dialog>
<confirm-dialog
ref="setRoleDialog"
@confirm="() => setRole(Number(setRoleId), Number(setRoleRole))"
>
<template #title>
Confirm to set role of &nbsp; <b>{{ setRoleName }}</b
>?
</template>
<template #text>
User <b>{{ setRoleName }}</b> will be set as
<i>{{ setRoleRole === 1 ? "admin" : "normal" }} user</i>. Do you really want to do this?
</template>
</confirm-dialog>
<v-scroll-y-transition class="fixed left-0 right-0 z-index-10000">
<v-card-actions v-if="showError">
<v-alert
@ -389,8 +459,10 @@ onActivated(() => {
:page-num="pageNum"
:loading="loading"
:show-pagination="pageNum > 1"
:enable-set-role="store.getters.getRole === 2"
@click:delete="(id: number) => handleClickDelete(id)"
@click:reset="(id: number) => handleClickReset(id)"
@click:set-role="(id: number, role: number) => handleClickSetRole(id, role)"
@click:export="handleClickExport"
@page-change="pageChange"
/>

View File

@ -6,4 +6,5 @@ export default {
SearchByUsername: "Search by username",
SearchByEmail: "Search by email",
OopsNoUser: "Oops! No user found",
IsAdmin: "admin",
};

View File

@ -6,4 +6,5 @@ export default {
SearchByUsername: "按用户名搜索",
SearchByEmail: "按邮箱搜索",
OopsNoUser: "哦豁!没有找到用户",
IsAdmin: "管理员",
};

View File

@ -19,6 +19,7 @@ function getProfile(): Promise<{
data: {
username: string;
email: string;
role: number;
};
}> {
return checkedFetch(`${BASE_URL}/profile`, {

View File

@ -3,12 +3,14 @@ import { getProfile } from "../../request/user";
interface AuthState {
loggedIn: boolean;
userName: string;
role: number;
}
const auth = {
state: {
loggedIn: false,
userName: "",
role: 0,
},
mutations: {
setLoggedIn(state: AuthState, loggedIn: boolean): void {
@ -17,16 +19,21 @@ const auth = {
setUserName(state: AuthState, userName: string): void {
state.userName = userName;
},
setRole(state: AuthState, role: number): void {
state.role = role;
},
},
actions: {
async login({ commit }: { commit: (arg0: string, arg1: unknown) => void }): Promise<void> {
const res = await getProfile();
commit("setLoggedIn", true);
commit("setUserName", res.data.username);
commit("setRole", res.data.role);
},
async logout({ commit }: { commit: (arg0: string, arg1: unknown) => void }): Promise<void> {
commit("setLoggedIn", false);
commit("setUserName", "");
commit("setRole", 0);
},
},
getters: {
@ -36,6 +43,9 @@ const auth = {
getUserName(state: AuthState): string {
return state.userName;
},
getRole(state: AuthState): number {
return state.role;
},
},
};

View File

@ -7,6 +7,7 @@ export interface User {
id: number;
username: string;
email: string;
role: number;
unverified_learnware_count: number;
verified_learnware_count: number;
}