重构用户权限
Some checks failed
Deploy / deploy (push) Has been cancelled

This commit is contained in:
2025-09-25 18:41:32 +08:00
parent 4429e2f018
commit efbca0e802
15 changed files with 619 additions and 187 deletions

123
docs/PERMISSIONS.md Normal file
View File

@@ -0,0 +1,123 @@
# 前端权限控制说明
本文档说明了前端如何根据后端权限设计实现权限控制。
## 权限等级
### 1. 普通用户 (Regular User)
- admin_type: "Regular User"
- problem_permission: "None"
- 只能访问前台功能,无法进入管理后台
### 2. 管理员 (Admin)
- admin_type: "Admin"
- problem_permission: "None" | "Own" | "All"
- 可以访问部分管理功能
### 3. 超级管理员 (Super Admin)
- admin_type: "Super Admin"
- problem_permission: "All" (自动设置)
- 可以访问所有管理功能
## 权限控制实现
### 1. 用户状态管理 (`src/shared/store/user.ts`)
```typescript
const isAdminRole = computed(() =>
user.value?.admin_type === USER_TYPE.ADMIN ||
user.value?.admin_type === USER_TYPE.SUPER_ADMIN
)
const isSuperAdmin = computed(() =>
user.value?.admin_type === USER_TYPE.SUPER_ADMIN
)
const hasProblemPermission = computed(() =>
user.value?.problem_permission !== PROBLEM_PERMISSION.NONE
)
```
### 2. 权限工具函数 (`src/utils/permissions.ts`)
提供了一系列权限检查函数:
- `canManageUsers`: 只有super_admin可以管理用户
- `canManageProblems`: 需要题目权限
- `canManageSystemConfig`: 只有super_admin可以管理系统配置
- 等等...
### 3. 路由权限控制 (`src/main.ts`)
在路由守卫中检查权限:
- `requiresAdmin`: 需要admin或super_admin权限
- `requiresSuperAdmin`: 只有super_admin可以访问
- `requiresProblemPermission`: 需要题目管理权限
### 4. 菜单权限控制 (`src/shared/layout/admin.vue`)
根据用户权限动态显示菜单项:
- admin和super_admin都可以看到题目管理
- 只有super_admin可以看到比赛管理、用户管理、系统设置、公告管理、评论管理、教程管理
## 各功能模块权限说明
### 用户管理 (需要 super_admin)
- 创建、编辑、删除用户
- 重置用户密码
- 封禁/解封用户
- 批量导入用户
### 题目管理 (需要 problem_permission)
- `problem_permission = "None"`: 无法管理题目
- `problem_permission = "Own"`: 只能管理自己创建的题目
- `problem_permission = "All"`: 可以管理所有题目
### 比赛管理 (super_admin)
- 创建、编辑、删除比赛
- 管理比赛题目
- 查看比赛数据
### 系统配置 (需要 super_admin)
- SMTP邮件配置
- 判题服务器管理
- 网站基本设置
- 测试用例清理
### 公告管理 (需要 super_admin)
- 创建、编辑、删除公告
### 评论管理 (需要 super_admin)
- 查看、删除用户评论
### 教程管理 (需要 super_admin)
- 创建、编辑、删除教程
## 权限检查流程
1. **路由级权限检查**: 在 `main.ts` 的路由守卫中进行
2. **组件级权限检查**: 在组件内部使用 `usePermissions()` 进行检查
3. **UI权限控制**: 根据权限动态显示/隐藏UI元素
## 使用示例
```vue
<script setup lang="ts">
import { usePermissions } from "~/utils/permissions"
const { canManageUsers, isSuperAdmin } = usePermissions()
// 权限检查
if (!canManageUsers.value) {
message.error("您没有权限访问此页面")
router.push("/admin")
}
</script>
<template>
<!-- 根据权限显示不同内容 -->
<n-button v-if="isSuperAdmin" type="primary">
超级管理员专用功能
</n-button>
</template>
```
## 注意事项
1. **前端权限控制只是UI层面的限制**,真正的权限控制在后端
2. **所有API调用都会在后端进行权限验证**
3. **权限信息来自后端用户接口**,前端不应该缓存或修改权限信息
4. **路由权限检查是异步的**,需要等待用户信息加载完成

View File

@@ -84,11 +84,11 @@ export function getContestProblem(id: number) {
export function getUserList(
offset = 0,
limit = 10,
admin = "0",
type = "",
keyword: string,
) {
return http.get("admin/user", {
params: { paging: true, offset, limit, keyword, admin },
params: { paging: true, offset, limit, keyword, type },
})
}

View File

@@ -2,6 +2,7 @@
import { NButton, NTag } from "naive-ui"
import { parseTime } from "~/utils/functions"
import { Server } from "~/utils/types"
import { usePermissions } from "~/utils/permissions"
import {
deleteJudgeServer,
editWebsite,
@@ -17,6 +18,14 @@ interface Testcase {
}
const message = useMessage()
const router = useRouter()
const { canManageSystemConfig } = usePermissions()
// 权限检查只有super_admin可以管理系统配置
if (!canManageSystemConfig.value) {
message.error("您没有权限访问此页面")
router.push("/admin")
}
const testcaseColumns: DataTableColumn<Testcase>[] = [
{ title: "测试用例 ID", key: "id" },

View File

@@ -3,6 +3,7 @@ import { NButton } from "naive-ui"
import { getRank } from "oj/api"
import Pagination from "~/shared/components/Pagination.vue"
import { useUserStore } from "~/shared/store/user"
import { usePermissions } from "~/utils/permissions"
import { getACRate } from "~/utils/functions"
import { Rank } from "~/utils/types"
import { getBaseInfo, randomUser10 } from "../api"
@@ -13,6 +14,13 @@ const contestCount = ref(0)
const userStore = useUserStore()
const router = useRouter()
const message = useMessage()
const { isSuperAdmin } = usePermissions()
// 权限检查只有super_admin可以访问管理员首页
if (!isSuperAdmin.value) {
message.error("您没有权限访问此页面")
router.push("/admin/problem/list")
}
const showModal = ref(false)
const luckyGuy = ref("")

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { USER_TYPE } from "~/utils/constants"
import { getUserRole } from "~/utils/functions"
import { User } from "~/utils/types"
@@ -6,7 +7,9 @@ interface Props {
user: User
}
const props = defineProps<Props>()
const isAdmin = computed(() => props.user.admin_type !== "Regular User")
const isNotRegularUser = computed(
() => props.user.admin_type !== USER_TYPE.REGULAR_USER,
)
</script>
<template>
<n-flex align="center">
@@ -14,7 +17,7 @@ const isAdmin = computed(() => props.user.admin_type !== "Regular User")
封号中
</n-tag>
<n-tag
v-if="isAdmin"
v-if="isNotRegularUser"
:type="getUserRole(props.user.admin_type).type"
size="small"
>

View File

@@ -1,8 +1,9 @@
<script setup lang="ts">
import { DataTableRowKey, SelectOption } from "naive-ui"
import Pagination from "~/shared/components/Pagination.vue"
import { parseTime } from "~/utils/functions"
import { parseTime, filterEmptyValue } from "~/utils/functions"
import { User } from "~/utils/types"
import { usePermissions } from "~/utils/permissions"
import {
deleteUsers,
editUser,
@@ -15,6 +16,15 @@ import Name from "./components/Name.vue"
import { USER_TYPE } from "~/utils/constants"
const message = useMessage()
const router = useRouter()
const route = useRoute()
const { canManageUsers } = usePermissions()
// 权限检查只有super_admin可以管理用户
if (!canManageUsers.value) {
message.error("您没有权限访问此页面")
router.push("/admin")
}
const total = ref(0)
const users = ref<User[]>([])
@@ -23,8 +33,14 @@ const query = reactive({
limit: 10,
page: 1,
keyword: "",
admin: false,
type: "",
})
const adminOptions = [
{ label: "全部用户", value: "" },
{ label: "管理员", value: USER_TYPE.ADMIN },
{ label: "超级管理员", value: USER_TYPE.SUPER_ADMIN },
]
const [create, toggleCreate] = useToggle(false)
const password = ref("")
const userIDs = ref<DataTableRowKey[]>([])
@@ -78,15 +94,27 @@ const columns: DataTableColumn<User>[] = [
]
const options: SelectOption[] = [
{ label: "普通", value: "Regular User" },
{ label: "管理员", value: "Admin" },
{ label: "超级管理员", value: "Super Admin" },
{ label: "普通", value: USER_TYPE.REGULAR_USER },
{ label: "管理员", value: USER_TYPE.ADMIN },
{ label: "超级管理员", value: USER_TYPE.SUPER_ADMIN },
]
function routerPush() {
router.push({
path: route.path,
query: filterEmptyValue(query),
})
}
async function listUsers() {
query.keyword = <string>route.query.keyword ?? ""
query.page = parseInt(<string>route.query.page) || 1
query.limit = parseInt(<string>route.query.limit) || 10
query.type = <string>route.query.type ?? ""
if (query.page < 1) query.page = 1
const offset = (query.page - 1) * query.limit
const isAdmin = query.admin ? "1" : "0"
const res = await getUserList(offset, query.limit, isAdmin, query.keyword)
const res = await getUserList(offset, query.limit, query.type, query.keyword)
total.value = res.data.total
users.value = res.data.results
}
@@ -127,7 +155,7 @@ function createNewUser() {
username: "",
real_name: "",
email: "",
admin_type: "Regular User",
admin_type: "Super Admin",
problem_permission: "",
create_time: new Date(),
last_login: new Date(),
@@ -177,7 +205,28 @@ async function handleEditUser() {
}
onMounted(listUsers)
watch(query, listUsers, { deep: true })
watch(() => query.page, routerPush)
watch(
() => [query.limit, query.keyword, query.type],
() => {
query.page = 1
routerPush()
},
)
watchDebounced(
() => query.keyword,
() => {
query.page = 1
routerPush()
},
{ debounce: 500, maxWait: 1000 },
)
watch(
() => route.name === "admin user list" && route.query,
(newVal) => {
if (newVal) listUsers()
},
)
</script>
<template>
@@ -200,8 +249,12 @@ watch(query, listUsers, { deep: true })
确定删除选中的用户吗删除后无法恢复
</n-popconfirm>
<n-flex align="center">
<span>超管出列</span>
<n-switch v-model:value="query.admin" />
<n-select
v-model:value="query.type"
:options="adminOptions"
placeholder="选择用户类型"
style="width: 120px"
/>
<div>
<n-input style="width: 200px" v-model:value="query.keyword" />
</div>

View File

@@ -31,17 +31,68 @@ const router = createRouter({
routes: [ojs, admins],
})
router.beforeEach((to, from, next) => {
router.beforeEach(async (to, from, next) => {
// 检查是否需要认证
if (to.matched.some((record) => record.meta.requiresAuth)) {
if (!storage.get(STORAGE_KEY.AUTHED)) {
toggleLogin(true)
next("/")
} else {
next()
return
}
} else {
next()
}
// 检查管理员权限
if (to.matched.some((record) =>
record.meta.requiresAdmin ||
record.meta.requiresSuperAdmin ||
record.meta.requiresProblemPermission
)) {
if (!storage.get(STORAGE_KEY.AUTHED)) {
toggleLogin(true)
next("/")
return
}
// 动态导入用户store来检查权限
const { useUserStore } = await import("./shared/store/user")
const userStore = useUserStore()
// 确保用户信息已加载
if (!userStore.user) {
try {
await userStore.getMyProfile()
} catch (error) {
next("/")
return
}
}
// 检查super admin权限
if (to.matched.some((record) => record.meta.requiresSuperAdmin)) {
if (!userStore.isSuperAdmin) {
next("/admin")
return
}
}
// 检查题目权限
else if (to.matched.some((record) => record.meta.requiresProblemPermission)) {
if (!userStore.hasProblemPermission) {
next("/admin")
return
}
}
// 检查基本admin权限
else if (to.matched.some((record) => record.meta.requiresAdmin)) {
if (!userStore.isAdminRole) {
next("/")
return
}
}
}
next()
})
ChartJS.register(

View File

@@ -70,6 +70,7 @@ async function listSubmissions() {
if (query.page < 1) query.page = 1
const offset = query.limit * (query.page - 1)
try {
const res = await getSubmissions({
...query,
myself: query.myself ? "1" : "0",
@@ -80,6 +81,11 @@ async function listSubmissions() {
})
submissions.value = res.data.results
total.value = res.data.total
} catch (error: any) {
if (error.data === "Problem doesn't exist") {
message.error("题目不存在")
}
}
}
async function getTodayCount() {

View File

@@ -107,113 +107,138 @@ export const admins: RouteRecordRaw = {
path: "",
name: "admin home",
component: () => import("~/admin/setting/home.vue"),
meta: { requiresAdmin: true },
},
// 只有super_admin可以访问的路由
{
path: "config",
name: "admin config",
component: () => import("admin/setting/config.vue"),
meta: { requiresSuperAdmin: true },
},
{
path: "user/list",
name: "admin user list",
component: () => import("admin/user/list.vue"),
meta: { requiresSuperAdmin: true },
},
{
path: "user/generate",
name: "admin user generate",
component: () => import("~/admin/user/generate.vue"),
meta: { requiresSuperAdmin: true },
},
// admin和super_admin都可以访问的路由 (需要题目权限)
{
path: "problem/list",
name: "admin problem list",
component: () => import("admin/problem/list.vue"),
meta: { requiresProblemPermission: true },
},
{
path: "problem/create",
name: "admin problem create",
component: () => import("admin/problem/detail.vue"),
meta: { requiresProblemPermission: true },
},
{
path: "problem/edit/:problemID",
name: "admin problem edit",
component: () => import("admin/problem/detail.vue"),
props: true,
meta: { requiresProblemPermission: true },
},
// admin和super_admin都可以访问的路由
{
path: "contest/list",
name: "admin contest list",
component: () => import("admin/contest/list.vue"),
meta: { requiresAdmin: true },
},
{
path: "contest/create",
name: "admin contest create",
component: () => import("admin/contest/detail.vue"),
meta: { requiresAdmin: true },
},
{
path: "contest/edit/:contestID",
name: "admin contest edit",
component: () => import("admin/contest/detail.vue"),
props: true,
meta: { requiresAdmin: true },
},
{
path: "contest/:contestID/problem/list",
name: "admin contest problem list",
component: () => import("admin/problem/list.vue"),
props: true,
meta: { requiresAdmin: true },
},
{
path: "contest/:contestID/problem/create",
name: "admin contest problem create",
component: () => import("admin/problem/detail.vue"),
props: true,
meta: { requiresAdmin: true },
},
{
path: "contest/:contestID/problem/edit/:problemID",
name: "admin contest problem edit",
component: () => import("admin/problem/detail.vue"),
props: true,
meta: { requiresAdmin: true },
},
// 只有super_admin可以访问的路由
{
path: "announcement/list",
name: "admin announcement list",
component: () => import("admin/announcement/list.vue"),
meta: { requiresSuperAdmin: true },
},
{
path: "announcement/create",
name: "admin announcement create",
component: () => import("admin/announcement/detail.vue"),
meta: { requiresSuperAdmin: true },
},
{
path: "announcement/edit/:announcementID",
name: "admin announcement edit",
component: () => import("admin/announcement/detail.vue"),
props: true,
meta: { requiresSuperAdmin: true },
},
{
path: "comment/list",
name: "admin comment list",
component: () => import("admin/communication/comments.vue"),
meta: { requiresSuperAdmin: true },
},
{
path: "message/list",
name: "admin message list",
component: () => import("admin/communication/messages.vue"),
meta: { requiresSuperAdmin: true },
},
{
path: "tutorial/list",
name: "admin tutorial list",
component: () => import("admin/tutorial/list.vue"),
meta: { requiresSuperAdmin: true },
},
{
path: "tutorial/create",
name: "admin tutorial create",
component: () => import("admin/tutorial/detail.vue"),
meta: { requiresSuperAdmin: true },
},
{
path: "tutorial/edit/:tutorialID",
name: "admin tutorial edit",
component: () => import("admin/tutorial/detail.vue"),
props: true,
meta: { requiresSuperAdmin: true },
},
],
}

View File

@@ -87,7 +87,12 @@ const menus = computed<MenuOption[]>(() => [
icon: renderIcon("streamline-emojis:palm-tree"),
},
{
label: () => h(RouterLink, { to: "/admin" }, { default: () => "后台" }),
label: () =>
h(
RouterLink,
{ to: userStore.isTheAdmin ? "/admin/problem/list" : "/admin" },
{ default: () => (userStore.isTheAdmin ? "我出的题" : "后台") },
),
show: userStore.isAdminRole,
key: "admin",
icon: renderIcon("streamline-emojis:ghost"),

View File

@@ -7,30 +7,55 @@ import { useUserStore } from "../store/user"
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const options: MenuOption[] = [
// 根据用户权限动态生成菜单选项
const options = computed<MenuOption[]>(() => {
const baseOptions: MenuOption[] = [
{
label: () => h(RouterLink, { to: "/" }, { default: () => "前台" }),
key: "return to OJ",
},
]
// admin 可以访问的功能
if (userStore.isTheAdmin) {
baseOptions.push({
label: () =>
h(RouterLink, { to: "/admin/problem/list" }, { default: () => "题目" }),
key: "admin problem list",
})
}
// super_admin 可以访问的功能
if (userStore.isSuperAdmin) {
baseOptions.push(
{
label: () => h(RouterLink, { to: "/admin" }, { default: () => "管理" }),
key: "admin home",
},
{
label: () =>
h(
RouterLink,
{ to: "/admin/problem/list" },
{ default: () => "题目" },
),
key: "admin problem list",
},
{
label: () =>
h(
RouterLink,
{ to: "/admin/contest/list" },
{ default: () => "比赛" },
),
key: "admin contest list",
},
{
label: () =>
h(RouterLink, { to: "/admin/config" }, { default: () => "设置" }),
key: "admin config",
},
{
label: () =>
h(RouterLink, { to: "/admin/problem/list" }, { default: () => "题目" }),
key: "admin problem list",
},
{
label: () =>
h(RouterLink, { to: "/admin/comment/list" }, { default: () => "评论" }),
key: "admin comment list",
},
{
label: () =>
h(RouterLink, { to: "/admin/user/list" }, { default: () => "用户" }),
@@ -38,8 +63,12 @@ const options: MenuOption[] = [
},
{
label: () =>
h(RouterLink, { to: "/admin/contest/list" }, { default: () => "比赛" }),
key: "admin contest list",
h(
RouterLink,
{ to: "/admin/comment/list" },
{ default: () => "评论" },
),
key: "admin comment list",
},
{
label: () =>
@@ -52,10 +81,18 @@ const options: MenuOption[] = [
},
{
label: () =>
h(RouterLink, { to: "/admin/tutorial/list" }, { default: () => "教程" }),
h(
RouterLink,
{ to: "/admin/tutorial/list" },
{ default: () => "教程" },
),
key: "admin tutorial list",
},
]
)
}
return baseOptions
})
const active = computed(() => (route.name as string) || "home")

View File

@@ -13,6 +13,9 @@ export const useUserStore = defineStore("user", () => {
user.value?.admin_type === USER_TYPE.ADMIN ||
user.value?.admin_type === USER_TYPE.SUPER_ADMIN,
)
const isTheAdmin = computed(
() => user.value?.admin_type === USER_TYPE.ADMIN,
)
const isSuperAdmin = computed(
() => user.value?.admin_type === USER_TYPE.SUPER_ADMIN,
)
@@ -37,6 +40,7 @@ export const useUserStore = defineStore("user", () => {
isFinished,
user,
isAdminRole,
isTheAdmin,
isSuperAdmin,
hasProblemPermission,
isAuthed,

View File

@@ -1,34 +1,30 @@
import { getTime, intervalToDuration, parseISO } from "date-fns"
import { getTime, intervalToDuration, parseISO, type Duration } from "date-fns"
import { User } from "./types"
import { USER_TYPE } from "./constants"
export function getACRate(acCount: number, totalCount: number) {
let rate = ""
if (totalCount === 0) rate = "0.00"
else {
if (acCount >= totalCount) rate = "100.00"
else rate = ((acCount / totalCount) * 100).toFixed(2)
}
return `${rate}%`
function calculateACRate(acCount: number, totalCount: number): string {
if (totalCount === 0) return "0.00"
if (acCount >= totalCount) return "100.00"
return ((acCount / totalCount) * 100).toFixed(2)
}
export function getACRateNumber(acCount: number, totalCount: number) {
let rate = ""
if (totalCount === 0) rate = "0.00"
else {
if (acCount >= totalCount) rate = "100.00"
else rate = ((acCount / totalCount) * 100).toFixed(2)
}
return parseFloat(rate)
export function getACRate(acCount: number, totalCount: number): string {
return `${calculateACRate(acCount, totalCount)}%`
}
export function filterEmptyValue(object: any) {
let query: any = {}
Object.keys(object).forEach((key) => {
if (object[key] || object[key] === 0 || object[key] === false) {
query[key] = object[key]
export function getACRateNumber(acCount: number, totalCount: number): number {
return parseFloat(calculateACRate(acCount, totalCount))
}
export function filterEmptyValue<T extends Record<string, any>>(
object: T,
): Partial<T> {
return Object.entries(object).reduce((query, [key, value]) => {
if (value != null && value !== "" && value !== undefined) {
query[key as keyof T] = value
}
})
return query
}, {} as Partial<T>)
}
export function getTagColor(
@@ -50,56 +46,52 @@ export function parseTime(utc: Date | string, format = "YYYY年M月D日") {
return time.value
}
function getDurationObject(start: Date | string, end: Date | string) {
return intervalToDuration({
start: getTime(parseISO(start.toString())),
end: getTime(parseISO(end.toString())),
})
}
function formatDurationUnits(
duration: Duration,
units: Array<{ key: keyof Duration; suffix: string }>,
): string {
return units
.filter(({ key }) => duration[key])
.map(({ key, suffix }) => duration[key] + suffix)
.join("")
}
export function duration(
start: Date | string,
end: Date | string,
showSeconds = false,
): string {
const duration = intervalToDuration({
start: getTime(parseISO(start.toString())),
end: getTime(parseISO(end.toString())),
})
let result = ""
if (duration.years) {
result += duration.years + "年"
}
if (duration.months) {
result += duration.months + "月"
}
if (duration.days) {
result += duration.days + "天"
}
if (duration.hours) {
result += duration.hours + "小时"
}
if (duration.minutes) {
result += duration.minutes + "分钟"
}
if (showSeconds && duration.seconds) {
result += duration.seconds + "秒"
}
return result
const durationObj = getDurationObject(start, end)
const units = [
{ key: "years" as const, suffix: "年" },
{ key: "months" as const, suffix: "月" },
{ key: "days" as const, suffix: "" },
{ key: "hours" as const, suffix: "小时" },
{ key: "minutes" as const, suffix: "分钟" },
...(showSeconds ? [{ key: "seconds" as const, suffix: "秒" }] : []),
]
return formatDurationUnits(durationObj, units)
}
export function durationToDays(
start: Date | string,
end: Date | string,
): string {
const duration = intervalToDuration({
start: getTime(parseISO(start.toString())),
end: getTime(parseISO(end.toString())),
})
let result = ""
if (duration.years) {
result += duration.years + "年"
}
if (duration.months) {
result += duration.months + "月"
}
if (duration.days) {
result += duration.days + "天"
}
return !!result ? result : "一天以内"
const durationObj = getDurationObject(start, end)
const units = [
{ key: "years" as const, suffix: "年" },
{ key: "months" as const, suffix: "月" },
{ key: "days" as const, suffix: "" },
]
const result = formatDurationUnits(durationObj, units)
return result || "一天以内"
}
export function secondsToDuration(seconds: number): string {
@@ -126,13 +118,14 @@ export function submissionTimeFormat(time: number | string | undefined) {
return time + "ms"
}
export function debounce(fn: Function, n = 100) {
let handle: any
return (...args: any[]) => {
if (handle) clearTimeout(handle)
handle = setTimeout(() => {
fn(...args)
}, n)
export function debounce<T extends (...args: any[]) => any>(
fn: T,
delay = 100,
): (...args: Parameters<T>) => void {
let timeoutId: ReturnType<typeof setTimeout>
return (...args: Parameters<T>) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => fn(...args), delay)
}
}
@@ -140,50 +133,50 @@ export function getUserRole(role: User["admin_type"]): {
type: "default" | "info" | "error"
tagString: "普通" | "管理员" | "超管"
} {
const obj: {
type: "default" | "info" | "error"
tagString: "普通" | "管理员" | "超管"
} = { type: "default", tagString: "普通" }
switch (role) {
case "Regular User":
obj.type = "default"
obj.tagString = "普通"
break
case "Admin":
obj.type = "info"
obj.tagString = "管理员"
break
case "Super Admin":
obj.type = "error"
obj.tagString = "超管"
break
const roleMap = {
[USER_TYPE.REGULAR_USER]: {
type: "default" as const,
tagString: "普通" as const,
},
[USER_TYPE.ADMIN]: { type: "info" as const, tagString: "管理员" as const },
[USER_TYPE.SUPER_ADMIN]: {
type: "error" as const,
tagString: "超管" as const,
},
}
return obj
return roleMap[role] || roleMap[USER_TYPE.REGULAR_USER]
}
export function unique<T>(arr: T[]) {
return arr.reduce((prev: T[], curr: T) => {
if (!prev.includes(curr)) {
prev.push(curr)
}
return prev
}, [])
export function unique<T>(arr: T[]): T[] {
return [...new Set(arr)]
}
export function encode(string?: string) {
export function encode(string?: string): string {
try {
return btoa(String.fromCharCode(...new TextEncoder().encode(string ?? "")))
} catch (error) {
console.error("编码失败:", error)
return ""
}
}
export function decode(bytes?: string) {
const latin = atob(bytes ?? "")
export function decode(bytes?: string): string {
try {
if (!bytes) return ""
const latin = atob(bytes)
return new TextDecoder("utf-8").decode(
Uint8Array.from({ length: latin.length }, (_, index) =>
latin.charCodeAt(index),
),
)
} catch (error) {
console.error("解码失败:", error)
return ""
}
}
export function getCSRFToken() {
export function getCSRFToken(): string {
if (typeof document === "undefined") {
return ""
}

113
src/utils/permissions.ts Normal file
View File

@@ -0,0 +1,113 @@
import { useUserStore } from "~/shared/store/user"
/**
* 权限检查工具函数
*/
export function usePermissions() {
const userStore = useUserStore()
return {
// 基本权限检查
isAuthenticated: computed(() => userStore.isAuthed),
isAdminRole: computed(() => userStore.isAdminRole),
isSuperAdmin: computed(() => userStore.isSuperAdmin),
hasProblemPermission: computed(() => userStore.hasProblemPermission),
// 功能权限检查
canManageUsers: computed(() => userStore.isSuperAdmin),
canManageAnnouncements: computed(() => userStore.isSuperAdmin),
canManageComments: computed(() => userStore.isSuperAdmin),
canManageTutorials: computed(() => userStore.isSuperAdmin),
canManageSystemConfig: computed(() => userStore.isSuperAdmin),
canSendMessages: computed(() => userStore.isSuperAdmin),
canManageProblems: computed(() => userStore.hasProblemPermission),
canManageContests: computed(() => userStore.isSuperAdmin),
// 题目权限细分检查
canManageAllProblems: computed(
() =>
userStore.user?.problem_permission === "All" || userStore.isSuperAdmin,
),
canManageOwnProblems: computed(
() =>
userStore.user?.problem_permission === "Own" && !userStore.isSuperAdmin,
),
// 获取用户权限级别描述
getUserPermissionLevel: computed(() => {
if (userStore.isSuperAdmin) return "超级管理员"
if (userStore.isAdminRole) return "管理员"
return "普通用户"
}),
// 获取题目权限描述
getProblemPermissionLevel: computed(() => {
if (!userStore.user) return "无权限"
switch (userStore.user.problem_permission) {
case "All":
return "管理所有题目"
case "Own":
return "管理自己的题目"
case "None":
return "无题目权限"
default:
return "无权限"
}
}),
}
}
/**
* 路由权限检查
*/
export function checkRoutePermission(routeName: string): boolean {
const userStore = useUserStore()
// 需要super admin权限的路由
const superAdminRoutes = [
"admin home",
"admin config",
"admin user list",
"admin user generate",
"admin announcement list",
"admin announcement create",
"admin announcement edit",
"admin comment list",
"admin message list",
"admin tutorial list",
"admin tutorial create",
"admin tutorial edit",
"admin contest list",
"admin contest create",
"admin contest edit",
"admin contest problem list",
"admin contest problem create",
"admin contest problem edit",
]
// 需要题目权限的路由
const problemPermissionRoutes = [
"admin problem list",
"admin problem create",
"admin problem edit",
]
// 需要基本admin权限的路由
const adminRoutes: string[] = ["admin problem list"]
if (superAdminRoutes.includes(routeName)) {
return userStore.isSuperAdmin
}
if (problemPermissionRoutes.includes(routeName)) {
return userStore.hasProblemPermission
}
if (adminRoutes.includes(routeName)) {
return userStore.isAdminRole
}
return true
}

View File

@@ -1,4 +1,4 @@
import { ContestStatus, ContestType } from "./constants"
import { ContestStatus, ContestType, USER_TYPE } from "./constants"
export interface Profile {
id: number
@@ -33,12 +33,14 @@ export interface Profile {
submission_number: number
}
export type UserAdminType = "Regular User" | "Admin" | "Super Admin"
export interface User {
id: number
username: string
real_name: string
email: string
admin_type: "Regular User" | "Super Admin" | "Admin"
admin_type: UserAdminType
problem_permission: string
create_time: Date
last_login: Date