添加管理员登录和管理面板组件,优化身份验证流程

This commit is contained in:
keqingmoe 2024-12-28 21:00:57 +08:00
parent 73efef985f
commit e078a1df93
6 changed files with 577 additions and 0 deletions

View File

@ -0,0 +1,27 @@
<template>
<v-sheet class="d-flex align-center justify-center flex-wrap text-center mx-auto px-4" v-if="authStore.adminToken == ''"
width="640" height="670">
<v-sheet class="v-center" height="100%">
<LoginForm></LoginForm>
</v-sheet>
</v-sheet>
<AdminPanel v-else></AdminPanel>
</template>
<script setup lang="ts">
import { useAuthStore } from '@/store/auth';
import LoginForm from './admin/LoginForm.vue';
import AdminPanel from './admin/AdminPanel.vue';
const authStore = useAuthStore();
</script>
<style lang="scss" scoped>
.v-center {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
</style>

View File

@ -0,0 +1,77 @@
<template>
<v-card subtitle="占位符占位符占位符" title="管理员面板" max-width="80%">
<v-card-item>
<v-chip class="ma-2" color="pink" label>
<v-icon icon="mdi-human" start></v-icon>
权限: 管理员
</v-chip>
</v-card-item>
<v-card-item>
<v-chip class="chips" color="orange" @click="logout">
退出登录
</v-chip>
<v-chip class="chips" color="blue" @click="dialogRepasswdShow = true">
修改密码
</v-chip>
</v-card-item>
<v-card-item>
<v-chip class="chips" color="blue" @click="dialogRepasswd2Show = true">
修改指定用户的密码
</v-chip>
<v-chip class="chips" color="red" @click="dialogDeleteAccountShow = true">
删除指定用户的账号
</v-chip>
</v-card-item>
</v-card>
<v-dialog v-model="dialogShow" width="auto">
<v-card max-width="400" prepend-icon="mdi-update" :text="dialogText" :title="dialogTitle">
<template v-slot:actions>
<v-btn class="ms-auto" text="Ok" @click="dialogShow = false, dialogClose()"></v-btn>
</template>
</v-card>
</v-dialog>
<RepasswdDialog v-model="dialogRepasswdShow"></RepasswdDialog>
<Repasswd2Dialog v-model="dialogRepasswd2Show"></Repasswd2Dialog>
<DeleteAccountDialog v-model="dialogDeleteAccountShow"></DeleteAccountDialog>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { useAuthStore } from '@/store/auth';
import axios, { AxiosError } from 'axios';
import RepasswdDialog from './RepasswdDialog.vue';
import Repasswd2Dialog from './Repasswd2Dialog .vue';
import DeleteAccountDialog from './DeleteAccountDialog.vue';
const authStore = useAuthStore();
const storedAdminToken = ref(authStore.adminToken);
const dialogShow = ref(false);
const dialogTitle = ref('');
const dialogText = ref('');
const dialogClose = ref(() => { });
const dialogRepasswdShow = ref(false);
const dialogRepasswd2Show = ref(false);
const dialogDeleteAccountShow = ref(false);
const dialog = (title: string, text: string) => {
dialogTitle.value = title;
dialogText.value = text;
dialogShow.value = true;
return new Promise(res => {
dialogClose.value = res as () => void;
});
};
const logout = async () => {
authStore.clearAdminToken();
}
</script>
<style lang="scss" scoped>
.chips {
margin: 5px;
}
</style>

View File

@ -0,0 +1,139 @@
<template>
<v-dialog v-model="dialogDeleteAccountShow" max-width="600">
<v-card prepend-icon="mdi-account">
<template v-slot:title>
删除账号
</template>
<template v-slot:subtitle>
<span style="color: red;">警告不可逆操作您正在进行很危险的事情</span>
</template>
<!-- <v-card-text>
</v-card-text> -->
<v-card-text>
<v-row dense>
<v-card-text>
在下方文本框中输入要删除的账号的 ID可以批量删除使用英文逗号分隔即可逗号前后不需要空格
</v-card-text>
<v-col cols="12" sm="12">
<v-text-field v-model="userIds" :rules="[() => userIds.length == 0 ? '不能为空' : true]" label="用户 ID"
prepend-inner-icon="mdi-account" type="text"></v-text-field>
</v-col>
</v-row>
<v-row dense>
<v-card-text style="color: red;">
请在下方文本框中一字不差地输入引号内的内容<span style="user-select: none;">{{ DialogDeleteAccountPromise }}</span>之后您才能删除账号
</v-card-text>
<v-col cols="12" sm="12">
<v-text-field v-model="dialogDeleteAccountPromise" :label="DialogDeleteAccountPromise"
prepend-inner-icon="mdi-delete" type="text"></v-text-field>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-btn text="取消" color="primary" variant="tonal" @click="dialogDeleteAccountShow = false"></v-btn>
<v-btn :readonly="dialogDeleteAccountPromise != DialogDeleteAccountPromise" color="red" variant="tonal"
@click="dialogDeleteAccountShow = false, deleteAccount()">
<span style="color: red;">确定</span>
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="dialogShow" width="auto">
<v-card max-width="400" prepend-icon="mdi-update" :text="dialogText" :title="dialogTitle">
<template v-slot:actions>
<v-btn class="ms-auto" text="Ok" @click="dialogShow = false, dialogClose()"></v-btn>
</template>
</v-card>
</v-dialog>
<v-dialog v-model="dialog2Show" width="auto">
<v-card max-width="400" prepend-icon="mdi-update" :text="dialogFailedListText" title="部分账号删除失败">
<template v-slot:actions>
<v-btn class="ms-auto" text="Ok" @click="deleteFailedList = []"></v-btn>
</template>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useAuthStore } from '@/store/auth';
import axios, { AxiosError } from 'axios';
const dialogShow = ref(false);
const dialogTitle = ref('');
const dialogText = ref('');
const dialogClose = ref(() => { });
const deleteFailedList = ref<string[]>([]);
const dialog2Show = computed(() => deleteFailedList.value.length > 0);
const dialogFailedListText = computed(() => {
return `ID 为 ${deleteFailedList.value.join()} 的账号删除失败。`
});
const dialog = (title: string, text: string) => {
dialogTitle.value = title;
dialogText.value = text;
dialogShow.value = true;
return new Promise(res => {
dialogClose.value = res as () => void;
});
};
const userIds = ref('');
const userIdArr = computed(() => {
return userIds.value.split(',');
});
const DialogDeleteAccountPromise = computed(() => {
return `我确定要删除这${userIdArr.value.length > 1 ? '些' : '个'}账号。`
});
const dialogDeleteAccountPromise = ref('');
const dialogDeleteAccountShow = defineModel({ default: true });
watch(dialogDeleteAccountShow, () => {
if (dialogDeleteAccountShow.value == true) {
dialogDeleteAccountPromise.value = '';
}
});
const authStore = useAuthStore();
const storedAdminToken = ref(authStore.adminToken);
type DeleteAccountResponse = { success?: string, error?: string };
const requestDeleteAccount = async (userId: string) => {
try {
const formData = new FormData;
formData.append("action", "delete");
formData.append("token", storedAdminToken.value);
formData.append("user_id", userId);
let res = await axios.post('/api/auth/admin', formData);
return res.data as DeleteAccountResponse;
} catch (e) {
let ex = e as AxiosError;
if (ex.response?.data) {
return ex.response?.data as DeleteAccountResponse;
} {
return { error: ex.message };
}
}
}
const deleteAccount = async () => {
if (userIdArr.value.length == 1) {
let res = await requestDeleteAccount(userIdArr.value[0]);
if (res?.error) {
await dialog('错误', `删除账号失败:${res.error}`);
} else {
await dialog('信息', `删除账号成功。`);
}
} else {
for (let userId of userIdArr.value) {
let res = await requestDeleteAccount(userId);
if (res?.error) {
deleteFailedList.value.push(userId);
}
}
}
}
</script>

View File

@ -0,0 +1,106 @@
<template>
<v-card class="mx-auto pa-12 pb-8" width="448" elevation="8" rounded="lg">
<v-form fast-fail @submit.prevent="submit">
<v-text-field v-model="userId" prepend-inner-icon="mdi-account-circle" disabled></v-text-field>
<v-divider :thickness="10" class="border-opacity-0"></v-divider>
<v-text-field v-model="password" :rules="passwordRules" :error-messages="passwordValidate" label="密码"
:append-inner-icon="visible ? 'mdi-eye' : 'mdi-eye-off'" :type="visible ? 'text' : 'password'"
@click:append-inner="visible = !visible" prepend-inner-icon="mdi-lock-outline"></v-text-field>
<v-divider :thickness="10" class="border-opacity-0"></v-divider>
<v-btn class="mb-8" color="blue" size="large" variant="tonal" type="submit" block>
登录
</v-btn>
<v-card-text class="text-center">
<a class="text-red text-decoration-none" @click="dialog('忘记密码', '请在后端服务器上修改密码。')">
忘记密码<v-icon icon="mdi-chevron-right"></v-icon>
</a>
</v-card-text>
</v-form>
</v-card>
<v-dialog v-model="dialogShow" width="auto">
<v-card max-width="400" prepend-icon="mdi-update" :text="dialogText" :title="dialogTitle">
<template v-slot:actions>
<v-btn class="ms-auto" text="Ok" @click="dialogShow = false, dialogClose()"></v-btn>
</template>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { useAuthStore } from '@/store/auth';
import axios, { AxiosError } from 'axios';
import { ref } from 'vue';
const visible = ref(false);
const authStore = useAuthStore();
const storedAdminToken = ref(authStore.adminToken);
const dialogShow = ref(false);
const dialogTitle = ref('');
const dialogText = ref('');
const dialogClose = ref(() => { });
const loading = ref(false);
const userId = ref('管理员');
const password = ref('');
const passwordValidate = ref<string>("");
type LoginResponse = { success?: string, token?: string, error?: string };
const dialog = (title: string, text: string) => {
dialogTitle.value = title;
dialogText.value = text;
dialogShow.value = true;
return new Promise(res => {
dialogClose.value = res as () => void;
});
};
const login = async () => {
try {
const formData = new FormData;
formData.append("action", "login");
formData.append("password", password.value);
let res = await axios.post('/api/auth/admin', formData);
return res.data as LoginResponse;
} catch (e) {
let ex = e as AxiosError;
if (ex.response?.data) {
return ex.response?.data as LoginResponse;
} {
return { error: ex.message };
}
}
};
const submit = async (event: SubmitEvent) => {
const results: any = await event;
if (results.valid) {
loading.value = true
let res = await login();
loading.value = false
if (res?.error) {
passwordValidate.value = res.error;
await dialog('错误', `登录失败:${res.error}`);
} else {
await dialog('信息', `登录成功,你好,管理员。`);
authStore.setAdminToken(res.token as string);
}
}
};
const passwordRules: any = [(value: string) => {
passwordValidate.value = '';
if (value?.length > 0) return true;
return '密码不能为空';
}];
</script>

View File

@ -0,0 +1,111 @@
<template>
<v-dialog v-model="dialogRepasswdShow" max-width="448">
<v-card class="mx-auto pa-12 pb-8" elevation="8" width="100%" rounded="lg">
<v-form fast-fail @submit.prevent="submit">
<v-text-field v-model="userId" prepend-inner-icon="mdi-account-circle" :rules="userIdRules"
label="账号"></v-text-field>
<v-divider :thickness="10" class="border-opacity-0"></v-divider>
<v-text-field v-model="password" :rules="passwordRules" label="新密码"
:append-inner-icon="visible ? 'mdi-eye' : 'mdi-eye-off'" :type="visible ? 'text' : 'password'"
@click:append-inner="visible = !visible" prepend-inner-icon="mdi-lock-outline"></v-text-field>
<v-divider :thickness="10" class="border-opacity-0"></v-divider>
<v-btn :loading="loading" class="mb-8" color="blue" size="large" type="submit" variant="tonal" block>
修改密码
</v-btn>
</v-form>
</v-card>
</v-dialog>
<v-dialog v-model="dialogShow" width="auto">
<v-card max-width="400" prepend-icon="mdi-update" :text="dialogText" :title="dialogTitle">
<template v-slot:actions>
<v-btn class="ms-auto" text="Ok" @click="dialogShow = false, dialogClose()"></v-btn>
</template>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useAuthStore } from '@/store/auth';
import axios, { AxiosError } from 'axios';
const dialogShow = ref(false);
const dialogTitle = ref('');
const dialogText = ref('');
const dialogClose = ref(() => { });
const dialog = (title: string, text: string) => {
dialogTitle.value = title;
dialogText.value = text;
dialogShow.value = true;
return new Promise(res => {
dialogClose.value = res as () => void;
});
};
const dialogRepasswdShow = defineModel({ default: true });
const loading = ref(false);
const userId = ref('');
const password = ref('');
const authStore = useAuthStore();
const storedAdminToken = ref(authStore.adminToken);
const visible = ref(false);
type RepasswdResponse = { success?: string, error?: string };
const requestRepasswd = async () => {
try {
const formData = new FormData;
formData.append("action", "repasswd2");
formData.append("token", storedAdminToken.value);
formData.append("user_id", userId.value);
formData.append("new_passwd", password.value);
let res = await axios.post('/api/auth/admin', formData);
return res.data as RepasswdResponse;
} catch (e) {
let ex = e as AxiosError;
if (ex.response?.data) {
return ex.response?.data as RepasswdResponse;
} {
return { error: ex.message };
}
}
}
const repasswd = async () => {
loading.value = true;
let res = await requestRepasswd();
loading.value = false;
if (res?.error) {
await dialog('错误', `修改密码失败:${res.error}`);
} else {
await dialog('信息', `修改密码成功。`);
}
}
const submit = async (event: SubmitEvent) => {
const results: any = await event;
if (results.valid) {
await repasswd();
}
};
const userIdRules: any = [(value: string) => {
if (value?.length > 0) return true;
return '账号不能为空';
}];
const passwordRules: any = [(value: string) => {
if (value?.length > 0) return true;
return '密码不能为空';
}];
</script>

View File

@ -0,0 +1,117 @@
<template>
<v-dialog v-model="dialogRepasswdShow" max-width="448">
<v-card class="mx-auto pa-12 pb-8" elevation="8" width="100%" rounded="lg">
<v-form fast-fail @submit.prevent="submit">
<v-text-field v-model="passwords[0]" :rules="passwordRules" label="原密码"
:append-inner-icon="visible[0] ? 'mdi-eye' : 'mdi-eye-off'" :type="visible[0] ? 'text' : 'password'"
@click:append-inner="visible[0] = !visible[0]" prepend-inner-icon="mdi-lock-outline"></v-text-field>
<v-divider :thickness="10" class="border-opacity-0"></v-divider>
<v-text-field v-model="passwords[1]" :rules="passwordRules" label="新密码"
:append-inner-icon="visible[1] ? 'mdi-eye' : 'mdi-eye-off'" :type="visible[1] ? 'text' : 'password'"
@click:append-inner="visible[1] = !visible[1]" prepend-inner-icon="mdi-lock-outline"></v-text-field>
<v-divider :thickness="10" class="border-opacity-0"></v-divider>
<v-text-field :rules="passwordRules2" label="重复输入新密码"
:append-inner-icon="visible[2] ? 'mdi-eye' : 'mdi-eye-off'" :type="visible[2] ? 'text' : 'password'"
@click:append-inner="visible[2] = !visible[2]" prepend-inner-icon="mdi-lock-outline"></v-text-field>
<v-divider :thickness="10" class="border-opacity-0"></v-divider>
<v-btn :loading="loading" class="mb-8" color="blue" size="large" type="submit" variant="tonal" block>
修改密码
</v-btn>
</v-form>
</v-card>
</v-dialog>
<v-dialog v-model="dialogShow" width="auto">
<v-card max-width="400" prepend-icon="mdi-update" :text="dialogText" :title="dialogTitle">
<template v-slot:actions>
<v-btn class="ms-auto" text="Ok" @click="dialogShow = false, dialogClose()"></v-btn>
</template>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useAuthStore } from '@/store/auth';
import axios, { AxiosError } from 'axios';
const dialogShow = ref(false);
const dialogTitle = ref('');
const dialogText = ref('');
const dialogClose = ref(() => { });
const dialog = (title: string, text: string) => {
dialogTitle.value = title;
dialogText.value = text;
dialogShow.value = true;
return new Promise(res => {
dialogClose.value = res as () => void;
});
};
const dialogRepasswdShow = defineModel({ default: true });
const loading = ref(false);
const passwords = ref(['', '']);
const authStore = useAuthStore();
const storedAdminToken = ref(authStore.adminToken);
const visible = ref([false, false, false]);
type RepasswdResponse = { success?: string, error?: string };
const requestRepasswd = async () => {
try {
const formData = new FormData;
formData.append("action", "repasswd");
formData.append("token", storedAdminToken.value);
formData.append("raw_passwd", passwords.value[0]);
formData.append("new_passwd", passwords.value[1]);
let res = await axios.post('/api/auth/admin', formData);
return res.data as RepasswdResponse;
} catch (e) {
let ex = e as AxiosError;
if (ex.response?.data) {
return ex.response?.data as RepasswdResponse;
} {
return { error: ex.message };
}
}
}
const repasswd = async () => {
loading.value = true;
let res = await requestRepasswd();
loading.value = false;
if (res?.error) {
await dialog('错误', `修改密码失败:${res.error}`);
} else {
await dialog('信息', `修改密码成功,请重新登录。`);
authStore.clearToken();
}
}
const submit = async (event: SubmitEvent) => {
const results: any = await event;
if (results.valid) {
await repasswd();
}
};
const passwordRules: any = [(value: string) => {
if (value?.length > 0) return true;
return '密码不能为空';
}];
const passwordRules2: any = [(value: string) => {
if (value == passwords.value[1]) return true;
return '两次输入需保持一致';
}];
</script>