refactor: 优化权限管理与登录流程,修复细节问题
1. 调整仪表盘图标从DeviceOutlined为MonitorOutlined 2. 新增403无权访问页面与路由配置 3. 完善权限存储与路由守卫逻辑 4. 修复用户信息获取与token持久化问题 5. 补充普通管理员权限初始化与补丁脚本 6. 优化后端用户服务与认证控制器逻辑 7. 调整登录表单交互与请求流程
This commit is contained in:
1 parent
3d7eb12103
commit
2534723842
9 files changed
+253
-44
No files matched your search
@@ -9,6 +9,12 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import("@/views/login/index.vue"),
|
||||
meta: { requiresAuth: false, title: "登录" }
|
||||
},
|
||||
{
|
||||
path: "/403",
|
||||
name: "Forbidden",
|
||||
component: () => import("@/views/403.vue"),
|
||||
meta: { requiresAuth: false, title: "无权访问" }
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
component: () => import("@/layouts/index.vue"),
|
||||
@@ -112,9 +118,20 @@ router.beforeEach(async (to, _from, next) => {
|
||||
|
||||
if (!permissionStore.userInfo) {
|
||||
try {
|
||||
await permissionStore.fetchUserInfo();
|
||||
const success = await permissionStore.fetchUserInfo();
|
||||
if (!success) {
|
||||
next({
|
||||
path: "/login",
|
||||
query: { redirect: to.fullPath }
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
permissionStore.logout();
|
||||
console.error('获取用户信息失败:', error);
|
||||
next({
|
||||
path: "/login",
|
||||
query: { redirect: to.fullPath }
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ interface UserInfo {
|
||||
|
||||
export const usePermissionStore = defineStore('permission', () => {
|
||||
const userInfo = ref<UserInfo | null>(null);
|
||||
const token = ref<string>('');
|
||||
const token = ref<string>(localStorage.getItem('access_token') || '');
|
||||
const permissions = ref<string[]>([]);
|
||||
|
||||
const isLoggedIn = computed(() => !!token.value);
|
||||
@@ -64,12 +64,28 @@ export const usePermissionStore = defineStore('permission', () => {
|
||||
if (res.code === 200) {
|
||||
userInfo.value = res.data;
|
||||
permissions.value = res.data.permissions || [];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function setToken(newToken: string) {
|
||||
token.value = newToken;
|
||||
localStorage.setItem('access_token', newToken);
|
||||
}
|
||||
|
||||
function setUserInfo(info: UserInfo | null) {
|
||||
userInfo.value = info;
|
||||
}
|
||||
|
||||
function setPermissions(perms: string[]) {
|
||||
permissions.value = perms;
|
||||
}
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
token,
|
||||
@@ -80,5 +96,8 @@ export const usePermissionStore = defineStore('permission', () => {
|
||||
login,
|
||||
logout,
|
||||
fetchUserInfo,
|
||||
setToken,
|
||||
setUserInfo,
|
||||
setPermissions,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="forbidden-container">
|
||||
<div class="forbidden-content">
|
||||
<div class="forbidden-icon">
|
||||
<LockOutlined />
|
||||
</div>
|
||||
<h1 class="forbidden-title">403</h1>
|
||||
<p class="forbidden-desc">抱歉,您没有访问此页面的权限</p>
|
||||
<p class="forbidden-hint">如需访问,请联系管理员分配相应权限</p>
|
||||
<a-button type="primary" @click="goBack">
|
||||
<ArrowLeftOutlined />
|
||||
返回登录
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { LockOutlined, ArrowLeftOutlined } from '@ant-design/icons-vue'
|
||||
import { Button } from 'ant-design-vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
function goBack() {
|
||||
router.push('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.forbidden-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.forbidden-content {
|
||||
text-align: center;
|
||||
padding: 60px 40px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.forbidden-icon {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin: 0 auto 24px;
|
||||
background: #fff2f0;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
:deep(.anticon) {
|
||||
font-size: 64px;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
|
||||
.forbidden-title {
|
||||
font-size: 72px;
|
||||
font-weight: 700;
|
||||
color: #1f1f1f;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.forbidden-desc {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.forbidden-hint {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin: 0 0 32px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -121,7 +121,7 @@ import {
|
||||
message,
|
||||
} from 'ant-design-vue';
|
||||
import {
|
||||
DeviceOutlined,
|
||||
MonitorOutlined,
|
||||
CheckCircleOutlined,
|
||||
AlertOutlined,
|
||||
TeamOutlined,
|
||||
@@ -149,7 +149,7 @@ const statCards = [
|
||||
{
|
||||
key: 'deviceTotal',
|
||||
title: '设备总数',
|
||||
icon: markRaw(DeviceOutlined),
|
||||
icon: markRaw(MonitorOutlined),
|
||||
color: '#1890ff',
|
||||
iconBg: '#e6f7ff',
|
||||
},
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<a-form ref="formRef" :model="formState" :rules="rules" size="large" @finish="handleLogin">
|
||||
<a-form-item name="username">
|
||||
<a-input ref="usernameInputRef" v-model:value="formState.username" placeholder="请输入用户名" allow-clear
|
||||
:disabled="loading" @pressEnter="focusPassword">
|
||||
:disabled="loading" @pressEnter="handlePasswordEnter">
|
||||
<template #prefix>
|
||||
<UserOutlined class="input-icon" />
|
||||
</template>
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<a-form-item name="password">
|
||||
<a-input-password ref="passwordInputRef" v-model:value="formState.password" placeholder="请输入密码" allow-clear
|
||||
:disabled="loading" @pressEnter="submitForm">
|
||||
:disabled="loading" @pressEnter="handleLogin">
|
||||
<template #prefix>
|
||||
<LockOutlined class="input-icon" />
|
||||
</template>
|
||||
@@ -68,6 +68,7 @@
|
||||
import { reactive, ref, onMounted, nextTick } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import axios from 'axios'
|
||||
import {
|
||||
UserOutlined,
|
||||
LockOutlined,
|
||||
@@ -112,33 +113,46 @@ onMounted(() => {
|
||||
})
|
||||
})
|
||||
|
||||
function focusPassword() {
|
||||
function handlePasswordEnter() {
|
||||
passwordInputRef.value?.focus()
|
||||
}
|
||||
|
||||
function submitForm() {
|
||||
formRef.value?.submit()
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
await permissionStore.login(formState.username, formState.password)
|
||||
const response = await axios.post('/api/auth/login', {
|
||||
username: formState.username,
|
||||
password: formState.password
|
||||
})
|
||||
|
||||
if (formState.rememberMe) {
|
||||
localStorage.setItem('remembered_username', formState.username)
|
||||
} else {
|
||||
localStorage.removeItem('remembered_username')
|
||||
}
|
||||
const res = response.data
|
||||
|
||||
message.success('登录成功!')
|
||||
if (res.code === 200) {
|
||||
const { accessToken, refreshToken, userInfo } = res.data
|
||||
|
||||
localStorage.setItem('access_token', accessToken)
|
||||
localStorage.setItem('refresh_token', refreshToken)
|
||||
|
||||
permissionStore.setToken(accessToken)
|
||||
permissionStore.setUserInfo(userInfo)
|
||||
permissionStore.setPermissions(userInfo.permissions || [])
|
||||
|
||||
if (formState.rememberMe) {
|
||||
localStorage.setItem('remembered_username', formState.username)
|
||||
} else {
|
||||
localStorage.removeItem('remembered_username')
|
||||
}
|
||||
|
||||
message.success('登录成功!')
|
||||
|
||||
setTimeout(() => {
|
||||
const redirect = (route.query.redirect as string) || '/'
|
||||
router.push(redirect)
|
||||
}, 500)
|
||||
} else {
|
||||
throw new Error(res.message || '登录失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('登录失败:', error)
|
||||
message.error(error.message || '登录失败,请检查用户名和密码')
|
||||
formState.password = ''
|
||||
nextTick(() => {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
-- ============================================================
|
||||
-- 普通管理员权限补丁 (用于已有数据库)
|
||||
-- 执行方式: 在MySQL中执行此SQL即可
|
||||
-- ============================================================
|
||||
USE smart_grid_iot;
|
||||
|
||||
-- 给普通管理员角色(role_id=2, code='admin')分配基础管理权限
|
||||
-- 先删除已存在的(避免重复), 再插入
|
||||
DELETE FROM `role_permissions` WHERE `role_id` = 2;
|
||||
|
||||
INSERT INTO `role_permissions` (`role_id`, `permission_id`) VALUES
|
||||
(2, (SELECT id FROM permissions WHERE code = 'dashboard')),
|
||||
(2, (SELECT id FROM permissions WHERE code = 'system')),
|
||||
(2, (SELECT id FROM permissions WHERE code = 'settings')),
|
||||
(2, (SELECT id FROM permissions WHERE code = 'user:list')),
|
||||
(2, (SELECT id FROM permissions WHERE code = 'user:create')),
|
||||
(2, (SELECT id FROM permissions WHERE code = 'user:update')),
|
||||
(2, (SELECT id FROM permissions WHERE code = 'role:list')),
|
||||
(2, (SELECT id FROM permissions WHERE code = 'system:basicSetting')),
|
||||
(2, (SELECT id FROM permissions WHERE code = 'system:securitySetting')),
|
||||
(2, (SELECT id FROM permissions WHERE code = 'log:operation'));
|
||||
|
||||
-- 验证结果
|
||||
SELECT r.name AS 角色名称, r.code AS 角色编码, COUNT(rp.permission_id) AS 权限数量
|
||||
FROM roles r
|
||||
LEFT JOIN role_permissions rp ON r.id = rp.role_id
|
||||
WHERE r.id = 2
|
||||
GROUP BY r.id;
|
||||
@@ -195,6 +195,19 @@ INSERT INTO `user_roles` (`user_id`, `role_id`) VALUES (1, 1);
|
||||
INSERT INTO `role_permissions` (`role_id`, `permission_id`)
|
||||
SELECT 1, id FROM permissions;
|
||||
|
||||
-- 给普通管理员角色(role_id=2)分配基础管理权限(不含删除/重置密码/角色管理等高危操作)
|
||||
INSERT INTO `role_permissions` (`role_id`, `permission_id`) VALUES
|
||||
(2, 1), -- dashboard 仪表盘
|
||||
(2, 2), -- system 系统管理(父菜单)
|
||||
(2, 3), -- settings 系统设置(父菜单)
|
||||
(2, 4), -- user:list 用户管理
|
||||
(2, 6), -- user:create 新增用户
|
||||
(2, 7), -- user:update 编辑用户
|
||||
(2, 5), -- role:list 角色管理(仅查看)
|
||||
(2, 14), -- system:basicSetting 基本设置
|
||||
(2, 15), -- system:securitySetting 安全设置
|
||||
(2, 16); -- log:operation 操作日志
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 初始化完成提示
|
||||
|
||||
@@ -5,6 +5,8 @@ import { TokenUtil } from "../util/token";
|
||||
import { PermissionService } from "../service/permission.service";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import User from "../entity/user.entity";
|
||||
import UserRole from "../entity/userRole.entity";
|
||||
import Role from "../entity/role.entity";
|
||||
|
||||
@Controller("/api/auth")
|
||||
export class AuthController {
|
||||
@@ -23,6 +25,12 @@ export class AuthController {
|
||||
@InjectRepository(User)
|
||||
userModel!: typeof User;
|
||||
|
||||
@InjectRepository(UserRole)
|
||||
userRoleModel!: typeof UserRole;
|
||||
|
||||
@InjectRepository(Role)
|
||||
roleModel!: typeof Role;
|
||||
|
||||
@Post("/login")
|
||||
async login(@Body() body: { username: string; password: string }) {
|
||||
try {
|
||||
@@ -102,7 +110,14 @@ export class AuthController {
|
||||
const decoded = this.tokenUtil.verifyToken(token);
|
||||
const userId = decoded.userId;
|
||||
|
||||
const user = await this.userModel.findByPk(userId);
|
||||
const user = await this.userModel.findByPk(userId, {
|
||||
include: [
|
||||
{
|
||||
model: this.userRoleModel,
|
||||
include: [this.roleModel]
|
||||
}
|
||||
]
|
||||
});
|
||||
if (!user) {
|
||||
return {
|
||||
code: 404,
|
||||
@@ -111,6 +126,7 @@ export class AuthController {
|
||||
}
|
||||
|
||||
const permissions = await this.permissionService.getUserPermissions(userId);
|
||||
const roles = (user as any).userRoles?.map((ur: any) => ur.role?.code).filter(Boolean) || [];
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
@@ -120,7 +136,7 @@ export class AuthController {
|
||||
username: user.username,
|
||||
nickname: user.nickname || user.username,
|
||||
avatar: user.avatar,
|
||||
roles: [],
|
||||
roles,
|
||||
permissions
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Provide, Inject } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import { Repository } from "sequelize-typescript";
|
||||
import User from "../entity/user.entity";
|
||||
import UserRole from "../entity/userRole.entity";
|
||||
import Role from "../entity/role.entity";
|
||||
@@ -9,17 +10,17 @@ import { hashPassword } from "../util/crypto";
|
||||
@Provide()
|
||||
export class UserService {
|
||||
@InjectRepository(User)
|
||||
userModel!: typeof User;
|
||||
userModel!: Repository<User>;
|
||||
|
||||
@InjectRepository(UserRole)
|
||||
userRoleModel!: typeof UserRole;
|
||||
userRoleModel!: Repository<UserRole>;
|
||||
|
||||
@InjectRepository(Role)
|
||||
roleModel!: typeof Role;
|
||||
roleModel!: Repository<Role>;
|
||||
|
||||
async list(query: { page?: number; pageSize?: number; keyword?: string; status?: number; roleId?: number }): Promise<any> {
|
||||
const page = query.page || 1;
|
||||
const pageSize = query.pageSize || 10;
|
||||
async list(query: { page?: any; pageSize?: any; keyword?: string; status?: number; roleId?: number }): Promise<any> {
|
||||
const page = Number(query.page) || 1;
|
||||
const pageSize = Number(query.pageSize) || 10;
|
||||
const where: any = {};
|
||||
|
||||
if (query.keyword) {
|
||||
@@ -32,11 +33,11 @@ export class UserService {
|
||||
|
||||
const includeOptions: any = [
|
||||
{
|
||||
model: this.userRoleModel,
|
||||
model: UserRole,
|
||||
required: !!query.roleId,
|
||||
include: [
|
||||
{
|
||||
model: this.roleModel,
|
||||
model: Role,
|
||||
where: query.roleId ? { id: query.roleId } : {},
|
||||
required: !!query.roleId
|
||||
}
|
||||
@@ -50,14 +51,21 @@ export class UserService {
|
||||
limit: pageSize,
|
||||
offset: (page - 1) * pageSize,
|
||||
order: [["created_at", "DESC"]],
|
||||
distinct: true
|
||||
distinct: true,
|
||||
raw: false
|
||||
});
|
||||
|
||||
const list = rows.map((user) => {
|
||||
const u = user.toJSON();
|
||||
delete u.password;
|
||||
return u;
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "获取用户列表成功",
|
||||
data: {
|
||||
list: rows,
|
||||
list,
|
||||
total: count,
|
||||
page: Number(page),
|
||||
pageSize: Number(pageSize)
|
||||
@@ -69,8 +77,8 @@ export class UserService {
|
||||
const user = await this.userModel.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
model: this.userRoleModel,
|
||||
include: [this.roleModel]
|
||||
model: UserRole,
|
||||
include: [Role]
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -83,10 +91,13 @@ export class UserService {
|
||||
};
|
||||
}
|
||||
|
||||
const userData = user.toJSON();
|
||||
delete userData.password;
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "获取用户详情成功",
|
||||
data: user
|
||||
data: userData
|
||||
};
|
||||
}
|
||||
|
||||
@@ -123,16 +134,19 @@ export class UserService {
|
||||
const createdUser = await this.userModel.findByPk(user.id, {
|
||||
include: [
|
||||
{
|
||||
model: this.userRoleModel,
|
||||
include: [this.roleModel]
|
||||
model: UserRole,
|
||||
include: [Role]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const userData = createdUser!.toJSON();
|
||||
delete userData.password;
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "创建用户成功",
|
||||
data: createdUser
|
||||
data: userData
|
||||
};
|
||||
}
|
||||
|
||||
@@ -168,16 +182,19 @@ export class UserService {
|
||||
const updatedUser = await this.userModel.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
model: this.userRoleModel,
|
||||
include: [this.roleModel]
|
||||
model: UserRole,
|
||||
include: [Role]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const userData = updatedUser!.toJSON();
|
||||
delete userData.password;
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "更新用户成功",
|
||||
data: updatedUser
|
||||
data: userData
|
||||
};
|
||||
}
|
||||
|
||||
@@ -242,10 +259,13 @@ export class UserService {
|
||||
|
||||
await user.update({ status });
|
||||
|
||||
const userData = user.toJSON();
|
||||
delete userData.password;
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "状态更新成功",
|
||||
data: user
|
||||
data: userData
|
||||
};
|
||||
}
|
||||
|
||||
@@ -255,7 +275,8 @@ export class UserService {
|
||||
order: [
|
||||
["sort_order", "ASC"],
|
||||
["created_at", "DESC"]
|
||||
]
|
||||
],
|
||||
raw: true
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
Reference in new issue
Block a user