forked from beimingwu/beimingwu
Merge pull request #71 from Learnware-LAMDA/front_super_admin
feat(frontend): add super role in admin page
This commit is contained in:
commit
01e19b4930
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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++) {
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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 <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"
|
||||
/>
|
||||
|
|
|
@ -6,4 +6,5 @@ export default {
|
|||
SearchByUsername: "Search by username",
|
||||
SearchByEmail: "Search by email",
|
||||
OopsNoUser: "Oops! No user found",
|
||||
IsAdmin: "admin",
|
||||
};
|
||||
|
|
|
@ -6,4 +6,5 @@ export default {
|
|||
SearchByUsername: "按用户名搜索",
|
||||
SearchByEmail: "按邮箱搜索",
|
||||
OopsNoUser: "哦豁!没有找到用户",
|
||||
IsAdmin: "管理员",
|
||||
};
|
||||
|
|
|
@ -19,6 +19,7 @@ function getProfile(): Promise<{
|
|||
data: {
|
||||
username: string;
|
||||
email: string;
|
||||
role: number;
|
||||
};
|
||||
}> {
|
||||
return checkedFetch(`${BASE_URL}/profile`, {
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ export interface User {
|
|||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
role: number;
|
||||
unverified_learnware_count: number;
|
||||
verified_learnware_count: number;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue