feat: 完善登录等接口鉴权
This commit is contained in:
67
apps/api/src/modules/auth/guards/owner-or-admin.guard.ts
Normal file
67
apps/api/src/modules/auth/guards/owner-or-admin.guard.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { User } from '../../user/user.entity';
|
||||
|
||||
/**
|
||||
* 资源所有者或管理员权限 Guard
|
||||
*
|
||||
* 用途:检查用户是否有权限访问资源
|
||||
* - 管理员(admin、super_admin)可以访问任何资源
|
||||
* - 普通用户只能访问自己的资源(通过比较 userId)
|
||||
*
|
||||
* 使用场景:
|
||||
* - 查询用户信息:管理员可以查询任何用户,普通用户只能查询自己
|
||||
* - 更新用户信息:管理员可以更新任何用户,普通用户只能更新自己
|
||||
* - 删除用户:管理员可以删除任何用户,普通用户只能删除自己
|
||||
*
|
||||
* 使用示例:
|
||||
* @Get(':id')
|
||||
* @UseGuards(JwtAuthGuard, OwnerOrAdminGuard)
|
||||
* findOneById(@Param('id') id: string) { ... }
|
||||
*/
|
||||
@Injectable()
|
||||
export class OwnerOrAdminGuard implements CanActivate {
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const request = context.switchToHttp().getRequest<{
|
||||
user?: User;
|
||||
params: { id?: string };
|
||||
}>();
|
||||
|
||||
const user = request.user;
|
||||
|
||||
if (!user) {
|
||||
throw new ForbiddenException('未授权访问');
|
||||
}
|
||||
|
||||
// 获取请求的资源ID(从路由参数中)
|
||||
const requestedId = request.params?.id;
|
||||
|
||||
if (!requestedId) {
|
||||
// 如果没有提供资源ID,只允许管理员访问
|
||||
const isAdmin =
|
||||
user.role === 'admin' || user.role === 'super_admin';
|
||||
if (!isAdmin) {
|
||||
throw new ForbiddenException('权限不足,需要管理员权限');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const requestedUserId = +requestedId;
|
||||
|
||||
// 检查权限:管理员可以访问任何资源,普通用户只能访问自己的资源
|
||||
const isAdmin = user.role === 'admin' || user.role === 'super_admin';
|
||||
const isOwner = user.userId === requestedUserId;
|
||||
|
||||
if (!isAdmin && !isOwner) {
|
||||
throw new ForbiddenException(
|
||||
'权限不足,只能访问自己的资源或需要管理员权限',
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
MinLength,
|
||||
MaxLength,
|
||||
Matches,
|
||||
IsIn,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
@@ -95,15 +94,4 @@ export class CreateUserDto {
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
unionId?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: '用户角色',
|
||||
example: 'user',
|
||||
enum: ['user', 'admin', 'super_admin'],
|
||||
default: 'user',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsIn(['user', 'admin', 'super_admin'])
|
||||
role?: string;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
Query,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
UseGuards,
|
||||
@@ -22,11 +21,11 @@ import { UserService } from './user.service';
|
||||
import { User } from './user.entity';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { QueryUserDto } from './dto/query-user.dto';
|
||||
import { ChangePasswordDto } from './dto/change-password.dto';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import { OwnerOrAdminGuard } from '../auth/guards/owner-or-admin.guard';
|
||||
|
||||
@ApiTags('user')
|
||||
@Controller('user')
|
||||
@@ -60,8 +59,8 @@ export class UserController {
|
||||
* 查询所有用户
|
||||
*/
|
||||
@Get()
|
||||
// @UseGuards(JwtAuthGuard, RolesGuard)
|
||||
// @Roles('admin', 'super_admin')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles('admin', 'super_admin')
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({
|
||||
summary: '查询所有用户',
|
||||
@@ -78,35 +77,38 @@ export class UserController {
|
||||
return this.userService.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 username 或 email 查询单个用户
|
||||
*/
|
||||
@Get('find')
|
||||
@ApiOperation({
|
||||
summary: '查询单个用户',
|
||||
description: '根据 username 或 email 查询用户',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '查询成功',
|
||||
type: User,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
description: '请求参数错误,必须提供 username 或 email',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: '用户不存在' })
|
||||
findOne(@Query() queryDto: QueryUserDto): Promise<User> {
|
||||
return this.userService.findOne(queryDto);
|
||||
}
|
||||
// /**
|
||||
// * 根据 username 或 email 查询单个用户
|
||||
// */
|
||||
// @Get('find')
|
||||
// @ApiOperation({
|
||||
// summary: '查询单个用户',
|
||||
// description: '根据 username 或 email 查询用户',
|
||||
// })
|
||||
// @ApiResponse({
|
||||
// status: 200,
|
||||
// description: '查询成功',
|
||||
// type: User,
|
||||
// })
|
||||
// @ApiResponse({
|
||||
// status: 400,
|
||||
// description: '请求参数错误,必须提供 username 或 email',
|
||||
// })
|
||||
// @ApiResponse({ status: 404, description: '用户不存在' })
|
||||
// findOne(@Query() queryDto: QueryUserDto): Promise<User> {
|
||||
// return this.userService.findOne(queryDto);
|
||||
// }
|
||||
|
||||
/**
|
||||
* 根据 ID 查询单个用户
|
||||
* 需要管理员权限或者是用户本人
|
||||
*/
|
||||
@Get(':id')
|
||||
@UseGuards(JwtAuthGuard, OwnerOrAdminGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({
|
||||
summary: '根据ID查询用户',
|
||||
description: '根据用户ID获取详细信息',
|
||||
description: '根据用户ID获取详细信息,需要管理员权限或者是用户本人',
|
||||
})
|
||||
@ApiParam({ name: 'id', description: '用户ID', type: Number })
|
||||
@ApiResponse({
|
||||
@@ -114,6 +116,11 @@ export class UserController {
|
||||
description: '查询成功',
|
||||
type: User,
|
||||
})
|
||||
@ApiResponse({ status: 401, description: '未授权' })
|
||||
@ApiResponse({
|
||||
status: 403,
|
||||
description: '权限不足,只能查询自己的信息或需要管理员权限',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: '用户不存在' })
|
||||
findOneById(@Param('id') id: string): Promise<User> {
|
||||
return this.userService.findOneById(+id);
|
||||
@@ -123,6 +130,8 @@ export class UserController {
|
||||
* 更新用户信息
|
||||
*/
|
||||
@Patch(':id')
|
||||
@UseGuards(JwtAuthGuard, OwnerOrAdminGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({
|
||||
summary: '更新用户信息',
|
||||
description: '更新用户信息,不允许修改 username、openId、unionId',
|
||||
@@ -147,6 +156,8 @@ export class UserController {
|
||||
*/
|
||||
@Patch(':id/password')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@UseGuards(JwtAuthGuard, OwnerOrAdminGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({
|
||||
summary: '修改密码',
|
||||
description: '修改用户密码,需要先验证旧密码',
|
||||
@@ -167,6 +178,8 @@ export class UserController {
|
||||
*/
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@UseGuards(JwtAuthGuard, OwnerOrAdminGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({
|
||||
summary: '删除用户',
|
||||
description: '软删除用户,将状态更新为 deleted',
|
||||
|
||||
@@ -91,6 +91,7 @@ export class UserService {
|
||||
);
|
||||
|
||||
// 创建用户
|
||||
// 注意:所有注册用户的 role 固定为 'user',不允许通过注册接口设置其他角色
|
||||
const user = this.userRepository.create({
|
||||
username: createUserDto.username,
|
||||
passwordHash,
|
||||
@@ -101,7 +102,7 @@ export class UserService {
|
||||
openId: createUserDto.openId,
|
||||
unionId: createUserDto.unionId,
|
||||
status: 'active',
|
||||
role: createUserDto.role || 'user', // 默认为普通用户
|
||||
role: 'user', // 固定为普通用户,不允许通过注册接口修改
|
||||
});
|
||||
|
||||
return this.userRepository.save(user);
|
||||
|
||||
Reference in New Issue
Block a user