管理员为用户添加临时权限

This commit is contained in:
Kunagisa 2025-07-05 00:24:11 +08:00
parent ddfc8e98c6
commit ebf7a5125b
5 changed files with 190 additions and 18 deletions

View File

@ -116,3 +116,84 @@ export const adminAddTempPrivilege = async (uuid, privilege, exp_time) => {
throw error;
}
}
/**
* 获取用户信息
* 路由: /user/get
* 方法: GET
* 参数: uuid | name | qq_code (优先级: uuid > name > qq_code)
* @param {Object} params - 查询参数 { uuid, name, qq_code }
* @returns {Promise<Object>} 用户信息对象
*/
export const getUserByInfo = async (params = {}) => {
try {
const response = await axiosInstance.get('/user/get', { params });
return response.data;
} catch (error) {
throw error;
}
}
/**
* 修改用户名
* 路由: /user/changename
* 方法: PUT
* 需要登录
* @param {string} name - 新用户名
* @returns {Promise<void>} 无返回值成功即为修改成功
*/
export const changeUserName = async (name) => {
try {
await axiosInstance.put('/user/changename', { name });
} catch (error) {
throw error;
}
}
/**
* 用户请求修改密码发送重置请求
* 路由: /user/resetpassword
* 方法: POST
* 需要登录
* @returns {Promise<void>} 无返回值成功即为请求成功
*/
export const requestResetPassword = async () => {
try {
await axiosInstance.post('/user/resetpassword');
} catch (error) {
throw error;
}
}
/**
* 用户修改密码
* 路由: /user/resetpassword
* 方法: PUT
* 需要登录
* @param {string} token - 请求token
* @param {string} password - 新密码
* @returns {Promise<void>} 无返回值成功即为修改成功
*/
export const resetPassword = async (token, password) => {
try {
await axiosInstance.put('/user/resetpassword', { token, password });
} catch (error) {
throw error;
}
}
/**
* 获取用户临时权限信息
* 路由: /user/temp_privilege
* 方法: GET
* 需要登录
* @returns {Promise<Object>} 返回一个包含临时权限信息的Promise对象
*/
export const getTempPrivilege = async () => {
try {
const response = await axiosInstance.get('/user/temp_privilege');
return response.data;
} catch (error) {
throw error;
}
}

View File

@ -3,7 +3,7 @@ import { hasValidToken, getUserInfo, logoutUser, getStoredUser } from '../utils/
import { justLoggedIn } from '../utils/authSessionState';
import EditorsMaps from '@/views/index/EditorsMaps.vue'
import mitt from 'mitt'
import { hasPrivilege } from '@/utils/privilege'
import { hasPrivilege, hasPrivilegeWithTemp } from '@/utils/privilege'
const routes = [
{
@ -22,7 +22,7 @@ const routes = [
path: 'demands',
name: 'DemandList',
component: () => import('@/views/index/DemandList.vue'),
meta: { requiresAuth: true, requiredPrivilege: ['lv-admin','lv-mod','lv-map','lv-user','lv-competitor'] }
meta: { requiresAuth: true, requiredPrivilege: ['lv-admin','lv-mod','lv-map','lv-competitor'] }
},
{
path: 'maps',
@ -88,13 +88,13 @@ const routes = [
path: 'PIC2TGA',
name: 'PIC2TGA',
component: () => import('@/views/index/PIC2TGA.vue'),
meta: { requiredPrivilege: ['lv-admin','lv-mod','lv-map','lv-user','lv-competitor'] }
meta: { requiredPrivilege: ['lv-admin','lv-mod','lv-map','lv-competitor'] }
},
{
path: 'terrainGenerate',
name: 'TerrainGenerate',
component: () => import('@/views/index/TerrainGenerate.vue'),
meta: { requiredPrivilege: ['lv-admin','lv-mod','lv-map','lv-user','lv-competitor'] }
meta: { requiredPrivilege: ['lv-admin','lv-mod','lv-map','lv-competitor'] }
}
]
},
@ -170,7 +170,7 @@ router.beforeEach(async (to, from, next) => {
// 权限校验
if (requiredPrivilege) {
const user = getStoredUser();
if (!user || !hasPrivilege(user.privilege, requiredPrivilege)) {
if (!user || !hasPrivilegeWithTemp(user, requiredPrivilege)) {
// 通过事件总线通知弹窗
eventBus.emit('no-privilege')
return next(false)

View File

@ -1,6 +1,7 @@
import axiosInstance from '../api/axiosConfig';
import router from '../router'; // 引入 Vue Router 实例
import { justLoggedIn } from './authSessionState'; // Import the flag
import { getTempPrivilege } from '../api/login'; // 导入获取临时权限的函数
const USER_INFO_URL = '/user'; // 获取用户信息的API端点
@ -42,8 +43,21 @@ export const getUserInfo = async () => {
throw new Error('No valid token found');
}
try {
// 首先获取用户基本信息
const response = await axiosInstance.get(USER_INFO_URL);
const user = response.data;
try {
// 尝试获取临时权限信息,如果失败不影响用户基本信息的返回
const tempPrivilegeResponse = await getTempPrivilege();
if (tempPrivilegeResponse && tempPrivilegeResponse.privilege) {
user.temp_privilege = tempPrivilegeResponse.privilege;
}
} catch (error) {
// 如果获取临时权限失败,只记录错误但不影响主流程
console.warn('Failed to fetch temp privilege:', error);
}
// 将获取到的用户信息存入 sessionStorage
sessionStorage.setItem('currentUser', JSON.stringify(user));
return user;

View File

@ -8,4 +8,34 @@ export function hasPrivilege(privilege, required) {
return required.some(r => privArr.includes(r));
}
return privArr.includes(required);
}
// 合并永久权限和临时权限
export function mergePrivileges(permanentPrivilege, tempPrivilege) {
if (!permanentPrivilege && !tempPrivilege) return '';
let mergedPrivileges = [];
// 添加永久权限
if (permanentPrivilege) {
mergedPrivileges.push(...permanentPrivilege.split(';'));
}
// 添加临时权限(如果存在)
if (tempPrivilege) {
mergedPrivileges.push(...tempPrivilege.split(';'));
}
// 去重并返回
return [...new Set(mergedPrivileges)].join(';');
}
// 检查用户是否有指定权限(包括临时权限)
export function hasPrivilegeWithTemp(userData, required) {
if (!userData) return false;
// 合并永久权限和临时权限
const mergedPrivileges = mergePrivileges(userData.privilege, userData.temp_privilege);
return hasPrivilege(mergedPrivileges, required);
}

View File

@ -2,10 +2,11 @@
import { computed, ref, onMounted, onUnmounted, defineAsyncComponent } from 'vue'
import { getUserInfo } from '@/utils/jwt'
import { useRouter } from 'vue-router'
import { hasPrivilege } from '@/utils/privilege'
import { hasPrivilege, hasPrivilegeWithTemp, mergePrivileges } from '@/utils/privilege'
import { eventBus } from '@/router/index.js'
import { requestTempPrivilege } from '@/api/login.js'
const PrivilegeRequestDialog = defineAsyncComponent(() => import('@/components/PrivilegeRequestDialog.vue'))
const SuccessDialog = defineAsyncComponent(() => import('@/components/SuccessDialog.vue'))
const isLoggedIn = computed(() => {
return !!localStorage.getItem('access_token') && !!currentUserData.value
@ -28,13 +29,16 @@ const userAvatarUrl = computed(() => {
return null
})
//
//
const userHighestPrivilege = computed(() => {
if (!currentUserData.value || !currentUserData.value.privilege) {
if (!currentUserData.value) {
return null
}
const privileges = currentUserData.value.privilege.split(';')
const mergedPrivileges = mergePrivileges(currentUserData.value.privilege, currentUserData.value.temp_privilege)
if (!mergedPrivileges) return null
const privileges = mergedPrivileges.split(';')
//
const privilegeLevels = ['lv-admin', 'lv-mod', 'lv-competitor', 'lv-map', 'lv-user']
@ -61,7 +65,14 @@ const privilegeDisplayName = computed(() => {
'lv-competitor': '竞技'
}
return displayNames[privilege] || privilege
// lv-user
const isTempPrivilege = privilege !== 'lv-user' &&
currentUserData.value &&
currentUserData.value.temp_privilege &&
currentUserData.value.temp_privilege.includes(privilege)
const baseName = displayNames[privilege] || privilege
return isTempPrivilege ? `临时:${baseName}` : baseName
})
//
@ -101,6 +112,8 @@ const toggleDropdown = () => {
showDropdown.value = !showDropdown.value
}
let privilegeCheckTimer = null
onMounted(() => {
if (localStorage.getItem('access_token')) {
getUserInfo().then(userInfo => {
@ -114,20 +127,49 @@ onMounted(() => {
//
document.addEventListener('click', handleClickOutside)
// 30
privilegeCheckTimer = setInterval(async () => {
if (localStorage.getItem('access_token') && currentUserData.value) {
try {
const userInfo = await getUserInfo()
if (userInfo) {
//
const hadTempPrivilege = currentUserData.value.temp_privilege && currentUserData.value.temp_privilege.trim() !== ''
const hasTempPrivilege = userInfo.temp_privilege && userInfo.temp_privilege.trim() !== ''
// maps
if (hadTempPrivilege && !hasTempPrivilege) {
router.push('/maps')
}
currentUserData.value = userInfo
}
} catch (error) {
console.log('权限检查失败:', error)
}
}
}, 30 * 1000) // 30
})
onUnmounted(() => {
//
document.removeEventListener('click', handleClickOutside)
//
if (privilegeCheckTimer) {
clearInterval(privilegeCheckTimer)
privilegeCheckTimer = null
}
})
//
const showTerrainList = true //
const showTerrainGenerate = computed(() => isLoggedIn.value && currentUserData.value)
const showWeaponMatch = computed(() => isLoggedIn.value && currentUserData.value && hasPrivilege(currentUserData.value.privilege, ['lv-admin', 'lv-mod']))
const showWeaponMatch = computed(() => isLoggedIn.value && currentUserData.value && hasPrivilegeWithTemp(currentUserData.value, ['lv-admin', 'lv-mod']))
const showPic2Tga = computed(() => isLoggedIn.value && currentUserData.value)
const showDemands = computed(() => isLoggedIn.value && currentUserData.value)
const showCompetition = computed(() => isLoggedIn.value && currentUserData.value && hasPrivilege(currentUserData.value.privilege, ['lv-admin', 'lv-competitor']))
const showCompetition = computed(() => isLoggedIn.value && currentUserData.value && hasPrivilegeWithTemp(currentUserData.value, ['lv-admin', 'lv-competitor']))
const showTerrainMenu = computed(() => showTerrainList || showTerrainGenerate.value)
const showOnlineToolsMenu = computed(() => showWeaponMatch.value || showPic2Tga.value)
@ -170,9 +212,9 @@ async function handlePrivilegeApply() {
}
function handleNavClick(route, privilegeList) {
if (!isLoggedIn.value || !currentUserData.value || !hasPrivilege(currentUserData.value.privilege, privilegeList)) {
// lv-admin
const order = ['lv-mod', 'lv-competitor', 'lv-map', 'lv-user']
if (!isLoggedIn.value || !currentUserData.value || !hasPrivilegeWithTemp(currentUserData.value, privilegeList)) {
// lv-adminlv-user
const order = ['lv-mod', 'lv-competitor', 'lv-map']
let privilegeKey = ''
for (const key of order) {
if (privilegeList && privilegeList.includes(key)) {
@ -215,7 +257,7 @@ function handleNavClick(route, privilegeList) {
<span class="nav-link">地形与纹理</span>
<div class="dropdown-content">
<router-link to="/terrain" class="nav-link">地形图列表</router-link>
<router-link v-if="isLoggedIn" to="/terrainGenerate" class="nav-link" @click.prevent="handleNavClick('/terrainGenerate', ['lv-admin','lv-mod','lv-map','lv-user','lv-competitor'])">地形纹理合成工具</router-link>
<router-link v-if="isLoggedIn" to="/terrainGenerate" class="nav-link" @click.prevent="handleNavClick('/terrainGenerate', ['lv-admin','lv-mod','lv-map','lv-competitor'])">地形纹理合成工具</router-link>
</div>
</div>
<!-- 仅登录后显示的菜单项 -->
@ -225,7 +267,7 @@ function handleNavClick(route, privilegeList) {
<span class="nav-link">在线工具</span>
<div class="dropdown-content">
<router-link to="/weapon-match" class="nav-link" @click.prevent="handleNavClick('/weapon-match', ['lv-admin','lv-mod'])">Weapon 匹配</router-link>
<router-link to="/PIC2TGA" class="nav-link" @click.prevent="handleNavClick('/PIC2TGA', ['lv-admin','lv-mod','lv-map','lv-user','lv-competitor'])">在线转tga工具</router-link>
<router-link to="/PIC2TGA" class="nav-link" @click.prevent="handleNavClick('/PIC2TGA', ['lv-admin','lv-mod','lv-map','lv-competitor'])">在线转tga工具</router-link>
</div>
</div>
<!-- 赛事信息 一级菜单 -->
@ -239,7 +281,7 @@ function handleNavClick(route, privilegeList) {
<div class="nav-dropdown">
<span class="nav-link">公共信息区</span>
<div class="dropdown-content">
<router-link to="/demands" class="nav-link" @click.prevent="handleNavClick('/demands', ['lv-admin','lv-mod','lv-map','lv-user','lv-competitor'])">办事大厅</router-link>
<router-link to="/demands" class="nav-link" @click.prevent="handleNavClick('/demands', ['lv-admin','lv-mod','lv-map','lv-competitor'])">办事大厅</router-link>
</div>
</div>
</template>
@ -292,6 +334,11 @@ function handleNavClick(route, privilegeList) {
@close="privilegeDialogVisible = false"
@apply="handlePrivilegeApply"
/>
<SuccessDialog
:visible="successDialog.visible"
:message="successDialog.message"
@close="successDialog.visible = false"
/>
</div>
</template>